diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 8c08ac9c20..69e33b978d 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,13 +3,13 @@ "isRoot": true, "tools": { "jetbrains.resharper.globaltools": { - "version": "2021.3.0", + "version": "2021.3.4", "commands": [ "jb" ] }, "regitlint": { - "version": "6.0.6", + "version": "6.0.8", "commands": [ "regitlint" ] @@ -21,7 +21,7 @@ ] }, "dotnet-reportgenerator-globaltool": { - "version": "5.0.0", + "version": "5.1.3", "commands": [ "reportgenerator" ] diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 0d51d15e75..150c8b45df 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -88,6 +88,14 @@ public sealed class AppDbContext : DbContext } ``` +## Creating a release (for maintainers) + +- Verify documentation is up-to-date +- Bump the package version in Directory.Build.props +- Create a GitHub release +- Update https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb to consume the new version and release +- Create a new branch in https://github.com/json-api-dotnet/MigrationGuide and update README.md in master + ## 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: @@ -95,8 +103,8 @@ public sealed class AppDbContext : DbContext 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 +- Bump the package version in Directory.Build.props +- 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 ed1eea959b..3e23a87e27 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,4 +7,3 @@ Closes #{ISSUE_NUMBER} - [ ] 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/CodingGuidelines.ruleset b/CodingGuidelines.ruleset index 05545fb55c..e647ad9e58 100644 --- a/CodingGuidelines.ruleset +++ b/CodingGuidelines.ruleset @@ -14,6 +14,7 @@ + @@ -21,7 +22,7 @@ - + diff --git a/Directory.Build.props b/Directory.Build.props index 2fd1da959a..1ceac951ee 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,10 +4,10 @@ 6.0.* 6.0.* 6.0.* - 4.* - 2.* + 4.1.* + 2.14.1 6.2.* - 5.0.0 + 5.0.2 $(MSBuildThisFileDirectory)CodingGuidelines.ruleset 9999 enable @@ -18,7 +18,7 @@ - + @@ -28,10 +28,14 @@ true + + $(NoWarn);AV2210 + + - 3.1.0 - 4.16.1 - 17.0.0 + 3.1.2 + 4.17.2 + 17.1.0 diff --git a/JsonApiDotNetCore.sln b/JsonApiDotNetCore.sln index e4b74f93aa..78464f8248 100644 --- a/JsonApiDotNetCore.sln +++ b/JsonApiDotNetCore.sln @@ -52,6 +52,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGeneratorTests", "tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCore.Annotations", "src\JsonApiDotNetCore.Annotations\JsonApiDotNetCore.Annotations.csproj", "{83FF097C-C8C6-477B-9FAB-DF99B84978B5}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabasePerTenantExample", "src\Examples\DatabasePerTenantExample\DatabasePerTenantExample.csproj", "{60334658-BE51-43B3-9C4D-F2BBF56C89CE}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCore.OpenApi", "src\JsonApiDotNetCore.OpenApi\JsonApiDotNetCore.OpenApi.csproj", "{71287D6F-6C3B-44B4-9FCA-E78FE3F02289}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenApiTests", "test\OpenApiTests\OpenApiTests.csproj", "{B693DE14-BB28-496F-AB39-B4E674ABCA80}" @@ -276,6 +278,18 @@ Global {83FF097C-C8C6-477B-9FAB-DF99B84978B5}.Release|x64.Build.0 = Release|Any CPU {83FF097C-C8C6-477B-9FAB-DF99B84978B5}.Release|x86.ActiveCfg = Release|Any CPU {83FF097C-C8C6-477B-9FAB-DF99B84978B5}.Release|x86.Build.0 = Release|Any CPU + {60334658-BE51-43B3-9C4D-F2BBF56C89CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60334658-BE51-43B3-9C4D-F2BBF56C89CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60334658-BE51-43B3-9C4D-F2BBF56C89CE}.Debug|x64.ActiveCfg = Debug|Any CPU + {60334658-BE51-43B3-9C4D-F2BBF56C89CE}.Debug|x64.Build.0 = Debug|Any CPU + {60334658-BE51-43B3-9C4D-F2BBF56C89CE}.Debug|x86.ActiveCfg = Debug|Any CPU + {60334658-BE51-43B3-9C4D-F2BBF56C89CE}.Debug|x86.Build.0 = Debug|Any CPU + {60334658-BE51-43B3-9C4D-F2BBF56C89CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60334658-BE51-43B3-9C4D-F2BBF56C89CE}.Release|Any CPU.Build.0 = Release|Any CPU + {60334658-BE51-43B3-9C4D-F2BBF56C89CE}.Release|x64.ActiveCfg = Release|Any CPU + {60334658-BE51-43B3-9C4D-F2BBF56C89CE}.Release|x64.Build.0 = Release|Any CPU + {60334658-BE51-43B3-9C4D-F2BBF56C89CE}.Release|x86.ActiveCfg = Release|Any CPU + {60334658-BE51-43B3-9C4D-F2BBF56C89CE}.Release|x86.Build.0 = Release|Any CPU {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Debug|Any CPU.Build.0 = Debug|Any CPU {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -358,6 +372,7 @@ Global {87D066F9-3540-4AC7-A748-134900969EE5} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} {0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} {83FF097C-C8C6-477B-9FAB-DF99B84978B5} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} + {60334658-BE51-43B3-9C4D-F2BBF56C89CE} = {026FBC6C-AF76-4568-9B87-EC73457899FD} {71287D6F-6C3B-44B4-9FCA-E78FE3F02289} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} {B693DE14-BB28-496F-AB39-B4E674ABCA80} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} diff --git a/JsonApiDotNetCore.sln.DotSettings b/JsonApiDotNetCore.sln.DotSettings index b5f9e22340..bd9975440c 100644 --- a/JsonApiDotNetCore.sln.DotSettings +++ b/JsonApiDotNetCore.sln.DotSettings @@ -632,8 +632,10 @@ $left$ = $right$; $collection$.IsNullOrEmpty() $collection$ == null || !$collection$.Any() WARNING + True True True + True True True True diff --git a/README.md b/README.md index 71e5f523e5..e2291f080e 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,17 @@ The ultimate goal of this library is to eliminate as much boilerplate as possibl These are some steps you can take to help you understand what this project is and how you can use it: -- [What is JSON:API and why should I use it?](https://nordicapis.com/the-benefits-of-using-json-api/) -- [The JSON:API specification](http://jsonapi.org/format/) -- Demo [Video](https://youtu.be/KAMuo6K7VcE), [Blog](https://dev.to/wunki/getting-started-5dkl) -- [Our documentation](https://www.jsonapi.net/) -- [Check out the example projects](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples) -- [Embercasts: Full Stack Ember with ASP.NET Core](https://www.embercasts.com/course/full-stack-ember-with-dotnet/watch/whats-in-this-course-cs) +### About +- [What is JSON:API and why should I use it?](https://nordicapis.com/the-benefits-of-using-json-api/) (blog, 2017) +- [Pragmatic JSON:API Design](https://www.youtube.com/watch?v=3jBJOga4e2Y) (video, 2017) +- [JSON:API and JsonApiDotNetCore](https://www.youtube.com/watch?v=79Oq0HOxyeI) (video, 2021) +- [JsonApiDotNetCore Release 4.0](https://dev.to/wunki/getting-started-5dkl) (blog, 2020) +- [JSON:API, .Net Core, EmberJS](https://youtu.be/KAMuo6K7VcE) (video, 2017) +- [Embercasts: Full Stack Ember with ASP.NET Core](https://www.embercasts.com/course/full-stack-ember-with-dotnet/watch/whats-in-this-course-cs) (paid course, 2017) + +### Official documentation +- [The JSON:API specification](https://jsonapi.org/format/1.1/) +- [JsonApiDotNetCore website](https://www.jsonapi.net/) - [Roadmap](ROADMAP.md) ## Related Projects @@ -79,7 +84,7 @@ See also our [versioning policy](./VERSIONING_POLICY.md). | | | Core 3.1 | 5 | | | | 5 | 5 | | | | 6 | 5 | -| v5.x | Pre-release | 6 | 6 | +| v5.x | Stable | 6 | 6 | ## Contributing diff --git a/ROADMAP.md b/ROADMAP.md index 559e0bfe7d..270ae294e6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,37 +4,12 @@ This document provides an overview of the direction this project is heading and > Disclaimer: This is an open source project. The available time of our contributors varies and therefore we do not plan release dates. This document expresses our current intent, which may change over time. -## v4.x +We have interest in the following topics. It's too soon yet to decide whether they'll make it into v5.x or in a later major version. -We've completed active development on v4.x, but we'll still fix important bugs or add small enhancements on request that don't require breaking changes nor lots of testing. - -## v5.x - -The need for breaking changes has blocked several efforts in the v4.x release, so now that we're starting work on v5, we're going to catch up. - -- [x] Remove Resource Hooks [#1025](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1025) -- [x] Update to .NET 5 with EF Core 5 [#1026](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1026) -- [x] Native many-to-many [#935](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/935) -- [x] Refactorings [#1027](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1027) [#944](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/944) -- [x] Tweak trace logging [#1033](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1033) -- [x] Instrumentation [#1032](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1032) -- [x] Optimized delete to-many [#1030](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1030) -- [x] Support System.Text.Json [#664](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/664) [#999](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/999) [1077](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1077) [1078](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1078) -- [x] Optimize IIdentifiable to ResourceObject conversion [#1028](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1028) [#1024](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1024) [#233](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/233) -- [x] Nullable reference types [#1029](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1029) -- [x] Improved paging links [#1010](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1010) -- [x] Configuration validation [#170](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/170) -- [x] Auto-generated controllers [#732](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/732) [#365](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/365) -- [x] Support .NET 6 with EF Core 6 [#1109](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1109) -- [x] Extract annotations into separate package [#730](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/730) - -Aside from the list above, we have interest in the following topics. It's too soon yet to decide whether they'll make it into v5.x or in a later major version. - -- Optimistic concurrency [#1004](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1004) +- Optimistic concurrency [#1119](https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/1119) - OpenAPI (Swagger) [#1046](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1046) - Fluent API [#776](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/776) -- Resource inheritance [#844](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/844) -- Idempotency +- Idempotency [#1132](https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/1132) ## Feedback diff --git a/appveyor.yml b/appveyor.yml index 8ff835a527..445f615ef9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -83,14 +83,14 @@ for: - provider: NuGet skip_symbols: false api_key: - secure: OBYPCgp3WCuwkDRMuZ9a4QcBdTja/lqlUwZ+Yl5VHqooSJRVTYKP5y15XK0fuHsZ + secure: S9fkLwmhi7w+DGouXYqYq/1PGocnYo8UBUKwv+BGpWHnzE6yHZEYth3j/XJ9Ydsa on: branch: master appveyor_repo_tag: true - provider: NuGet skip_symbols: false api_key: - secure: OBYPCgp3WCuwkDRMuZ9a4QcBdTja/lqlUwZ+Yl5VHqooSJRVTYKP5y15XK0fuHsZ + secure: S9fkLwmhi7w+DGouXYqYq/1PGocnYo8UBUKwv+BGpWHnzE6yHZEYth3j/XJ9Ydsa on: branch: /release\/.+/ appveyor_repo_tag: true diff --git a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs index 6b716a5401..12f5c2e788 100644 --- a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs +++ b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs @@ -127,11 +127,19 @@ protected override IEvaluatedIncludeCache CreateEvaluatedIncludeCache(IResourceG RelationshipAttribute multi4 = resourceAType.GetRelationshipByPropertyName(nameof(OutgoingResource.Multi4)); RelationshipAttribute multi5 = resourceAType.GetRelationshipByPropertyName(nameof(OutgoingResource.Multi5)); - ImmutableArray chain = ImmutableArray.Create(single2, single3, multi4, multi5); - IEnumerable chains = new ResourceFieldChainExpression(chain).AsEnumerable(); - - var converter = new IncludeChainConverter(); - IncludeExpression include = converter.FromRelationshipChains(chains); + var include = new IncludeExpression(new HashSet + { + new(single2, new HashSet + { + new(single3, new HashSet + { + new(multi4, new HashSet + { + new(multi5) + }.ToImmutableHashSet()) + }.ToImmutableHashSet()) + }.ToImmutableHashSet()) + }.ToImmutableHashSet()); var cache = new EvaluatedIncludeCache(); cache.Set(include); diff --git a/benchmarks/Serialization/SerializationBenchmarkBase.cs b/benchmarks/Serialization/SerializationBenchmarkBase.cs index befa5049e8..e1bcb10843 100644 --- a/benchmarks/Serialization/SerializationBenchmarkBase.cs +++ b/benchmarks/Serialization/SerializationBenchmarkBase.cs @@ -180,9 +180,9 @@ public Task OnSetToManyRelationshipAsync(TResource leftResource, HasM return Task.CompletedTask; } - public Task OnAddToRelationshipAsync(TId leftResourceId, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + public Task OnAddToRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, CancellationToken cancellationToken) - where TResource : class, IIdentifiable + where TResource : class, IIdentifiable { return Task.CompletedTask; } @@ -245,7 +245,7 @@ public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship private sealed class FakeMetaBuilder : IMetaBuilder { - public void Add(IReadOnlyDictionary values) + public void Add(IDictionary values) { } diff --git a/docs/docfx.json b/docs/docfx.json index 98e83101a5..7fdafa0fe5 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -3,7 +3,7 @@ { "src": [ { - "files": [ "**/JsonApiDotNetCore.csproj" ], + "files": [ "**/JsonApiDotNetCore.csproj","**/JsonApiDotNetCore.Annotations.csproj" ], "src": "../" } ], diff --git a/docs/usage/errors.md b/docs/usage/errors.md index 67ce6fde17..955612dc67 100644 --- a/docs/usage/errors.md +++ b/docs/usage/errors.md @@ -1,13 +1,13 @@ # Errors -Errors returned will contain only the properties that are set on the `Error` class. Custom fields can be added through `Error.Meta`. -You can create a custom error by throwing a `JsonApiException` (which accepts an `Error` instance), or returning an `Error` instance from an `ActionResult` in a controller. -Please keep in mind that JSON:API requires Title to be a generic message, while Detail should contain information about the specific problem occurence. +Errors returned will contain only the properties that are set on the `ErrorObject` class. Custom fields can be added through `ErrorObject.Meta`. +You can create a custom error by throwing a `JsonApiException` (which accepts an `ErrorObject` instance), or returning an `ErrorObject` instance from an `ActionResult` in a controller. +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) +return Conflict(new ErrorObject(HttpStatusCode.Conflict) { Title = "Target resource was modified by another user.", Detail = $"User {userName} changed the {resourceField} field on {resourceName} resource." @@ -17,7 +17,7 @@ return Conflict(new Error(HttpStatusCode.Conflict) From other code: ```c# -throw new JsonApiException(new Error(HttpStatusCode.Conflict) +throw new JsonApiException(new ErrorObject(HttpStatusCode.Conflict) { Title = "Target resource was modified by another user.", Detail = $"User {userName} changed the {resourceField} field on {resourceName} resource." @@ -75,7 +75,7 @@ public class CustomExceptionHandler : ExceptionHandler { return new[] { - new Error(HttpStatusCode.Conflict) + new ErrorObject(HttpStatusCode.Conflict) { Title = "Product is temporarily available.", Detail = $"Product {productOutOfStock.ProductId} " + diff --git a/docs/usage/extensibility/resource-definitions.md b/docs/usage/extensibility/resource-definitions.md index 8696811e16..af4f8a27c5 100644 --- a/docs/usage/extensibility/resource-definitions.md +++ b/docs/usage/extensibility/resource-definitions.md @@ -200,7 +200,7 @@ public class EmployeeDefinition : JsonApiResourceDefinition if (existingIncludes.Any(include => include.Relationship.Property.Name == nameof(Employee.Manager))) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Including the manager of employees is not permitted." }); diff --git a/docs/usage/reading/filtering.md b/docs/usage/reading/filtering.md index dab1fdb6e2..a1c1215ccd 100644 --- a/docs/usage/reading/filtering.md +++ b/docs/usage/reading/filtering.md @@ -24,6 +24,7 @@ Expressions are composed using the following functions: | Ends with text | `endsWith` | `?filter=endsWith(description,'End')` | | Equals one value from set | `any` | `?filter=any(chapter,'Intro','Summary','Conclusion')` | | Collection contains items | `has` | `?filter=has(articles)` | +| Type-check derived type (v5) | `isType` | `?filter=isType(,men)` | | Negation | `not` | `?filter=not(equals(lastName,null))` | | Conditional logical OR | `or` | `?filter=or(has(orders),has(invoices))` | | Conditional logical AND | `and` | `?filter=and(has(orders),has(invoices))` | @@ -86,6 +87,32 @@ GET /customers?filter=has(orders,not(equals(status,'Paid'))) HTTP/1.1 Which returns only customers that have at least one unpaid order. +_since v5.0_ + +Use the `isType` filter function to perform a type check on a derived type. You can pass a nested filter, where the derived fields are accessible. + +Only return men: +```http +GET /humans?filter=isType(,men) HTTP/1.1 +``` + +Only return men with beards: +```http +GET /humans?filter=isType(,men,equals(hasBeard,'true')) HTTP/1.1 +``` + +The first parameter of `isType` can be used to perform the type check on a to-one relationship path. + +Only return people whose best friend is a man with children: +```http +GET /humans?filter=isType(bestFriend,men,has(children)) HTTP/1.1 +``` + +Only return people who have at least one female married child: +```http +GET /humans?filter=has(children,isType(,woman,not(equals(husband,null)))) HTTP/1.1 +``` + # Legacy filters The next section describes how filtering worked in versions prior to v4.0. They are always applied on the set of resources being requested (no nesting). diff --git a/docs/usage/resources/inheritance.md b/docs/usage/resources/inheritance.md new file mode 100644 index 0000000000..47cf85ca67 --- /dev/null +++ b/docs/usage/resources/inheritance.md @@ -0,0 +1,409 @@ +# Resource inheritance + +_since v5.0_ + +Resource classes can be part of a type hierarchy. For example: + +```c# +#nullable enable + +public abstract class Human : Identifiable +{ + [Attr] + public string Name { get; set; } = null!; + + [HasOne] + public Man? Father { get; set; } + + [HasOne] + public Woman? Mother { get; set; } + + [HasMany] + public ISet Children { get; set; } = new HashSet(); + + [HasOne] + public Human? BestFriend { get; set; } +} + +public sealed class Man : Human +{ + [Attr] + public bool HasBeard { get; set; } + + [HasOne] + public Woman? Wife { get; set; } +} + +public sealed class Woman : Human +{ + [Attr] + public string? MaidenName { get; set; } + + [HasOne] + public Man? Husband { get; set; } +} +``` + +## Reading data + +You can access them through base or derived endpoints. + +```http +GET /humans HTTP/1.1 + +{ + "data": [ + { + "type": "women", + "id": "1", + "attributes": { + "maidenName": "Smith", + "name": "Jane Doe" + }, + "relationships": { + "husband": { + "links": { + "self": "/women/1/relationships/husband", + "related": "/women/1/husband" + } + }, + "father": { + "links": { + "self": "/women/1/relationships/father", + "related": "/women/1/father" + } + }, + "mother": { + "links": { + "self": "/women/1/relationships/mother", + "related": "/women/1/mother" + } + }, + "children": { + "links": { + "self": "/women/1/relationships/children", + "related": "/women/1/children" + } + }, + "bestFriend": { + "links": { + "self": "/women/1/relationships/bestFriend", + "related": "/women/1/bestFriend" + } + } + }, + "links": { + "self": "/women/1" + } + }, + { + "type": "men", + "id": "2", + "attributes": { + "hasBeard": true, + "name": "John Doe" + }, + "relationships": { + "wife": { + "links": { + "self": "/men/2/relationships/wife", + "related": "/men/2/wife" + } + }, + "father": { + "links": { + "self": "/men/2/relationships/father", + "related": "/men/2/father" + } + }, + "mother": { + "links": { + "self": "/men/2/relationships/mother", + "related": "/men/2/mother" + } + }, + "children": { + "links": { + "self": "/men/2/relationships/children", + "related": "/men/2/children" + } + }, + "bestFriend": { + "links": { + "self": "/men/2/relationships/bestFriend", + "related": "/men/2/bestFriend" + } + } + }, + "links": { + "self": "/men/2" + } + } + ] +} +``` + +### Spare fieldsets + +If you only want to retrieve the fields from the base type, you can use [sparse fieldsets](~/usage/reading/sparse-fieldset-selection.md). + +```http +GET /humans?fields[men]=name,children&fields[women]=name,children HTTP/1.1 +``` + +### Includes + +Relationships on derived types can be included without special syntax. + +```http +GET /humans?include=husband,wife,children HTTP/1.1 +``` + +### Sorting + +Just like includes, you can sort on derived attributes and relationships. + +```http +GET /humans?sort=maidenName,wife.name HTTP/1.1 +``` + +This returns all women sorted by their maiden names, followed by all men sorted by the name of their wife. + +To accomplish the same from a [Resource Definition](~/usage/extensibility/resource-definitions.md), upcast to the derived type: + +```c# +public override SortExpression OnApplySort(SortExpression? existingSort) +{ + return CreateSortExpressionFromLambda(new PropertySortOrder + { + (human => ((Woman)human).MaidenName, ListSortDirection.Ascending), + (human => ((Man)human).Wife!.Name, ListSortDirection.Ascending) + }); +} +``` + +### Filtering + +Use the `isType` filter function to perform a type check on a derived type. You can pass a nested filter, where the derived fields are accessible. + +Only return men: +```http +GET /humans?filter=isType(,men) HTTP/1.1 +``` + +Only return men with beards: +```http +GET /humans?filter=isType(,men,equals(hasBeard,'true')) HTTP/1.1 +``` + +The first parameter of `isType` can be used to perform the type check on a to-one relationship path. + +Only return people whose best friend is a man with children: +```http +GET /humans?filter=isType(bestFriend,men,has(children)) HTTP/1.1 +``` + +Only return people who have at least one female married child: +```http +GET /humans?filter=has(children,isType(,woman,not(equals(husband,null)))) HTTP/1.1 +``` + +## Writing data + +Just like reading data, you can use base or derived endpoints. When using relationships in request bodies, you can use base or derived types as well. +The only exception is that you cannot use an abstract base type in the request body when creating or updating a resource. + +For example, updating an attribute and relationship can be done at an abstract endpoint, but its body requires non-abstract types: + +```http +PATCH /humans/2 HTTP/1.1 + +{ + "data": { + "type": "men", + "id": "2", + "attributes": { + "hasBeard": false + }, + "relationships": { + "wife": { + "data": { + "type": "women", + "id": "1" + } + } + } + } +} +``` + +Updating a relationship does allow abstract types. For example: + +```http +PATCH /humans/1/relationships/children HTTP/1.1 + +{ + "data": [ + { + "type": "humans", + "id": "2" + } + ] +} +``` + +### Request pipeline + +The `TResource` type parameter used in controllers, resource services and resource repositories always matches the used endpoint. +But when JsonApiDotNetCore sees usage of a type from a type hierarchy, it fetches the stored types and updates `IJsonApiRequest` accordingly. +As a result, `TResource` can be different from what `IJsonApiRequest.PrimaryResourceType` returns. + +For example, on the request: +```http + GET /humans/1 HTTP/1.1 +``` + +JsonApiDotNetCore runs `IResourceService`, but `IJsonApiRequest.PrimaryResourceType` returns `Woman` +if human with ID 1 is stored as a woman in the underlying data store. + +Even with a simple type hierarchy as used here, lots of possible combinations quickly arise. For example, changing someone's best friend can be done using the following requests: +- `PATCH /humans/1/ { "data": { relationships: { bestFriend: { type: "women" ... } } } }` +- `PATCH /humans/1/ { "data": { relationships: { bestFriend: { type: "men" ... } } } }` +- `PATCH /women/1/ { "data": { relationships: { bestFriend: { type: "women" ... } } } }` +- `PATCH /women/1/ { "data": { relationships: { bestFriend: { type: "men" ... } } } }` +- `PATCH /men/2/ { "data": { relationships: { bestFriend: { type: "women" ... } } } }` +- `PATCH /men/2/ { "data": { relationships: { bestFriend: { type: "men" ... } } } }` +- `PATCH /humans/1/relationships/bestFriend { "data": { type: "human" ... } }` +- `PATCH /humans/1/relationships/bestFriend { "data": { type: "women" ... } }` +- `PATCH /humans/1/relationships/bestFriend { "data": { type: "men" ... } }` +- `PATCH /women/1/relationships/bestFriend { "data": { type: "human" ... } }` +- `PATCH /women/1/relationships/bestFriend { "data": { type: "women" ... } }` +- `PATCH /women/1/relationships/bestFriend { "data": { type: "men" ... } }` +- `PATCH /men/2/relationships/bestFriend { "data": { type: "human" ... } }` +- `PATCH /men/2/relationships/bestFriend { "data": { type: "women" ... } }` +- `PATCH /men/2/relationships/bestFriend { "data": { type: "men" ... } }` + +Because of all the possible combinations, implementing business rules in the pipeline is a no-go. +Resource definitions provide a better solution, see below. + +### Resource definitions + +In contrast to the request pipeline, JsonApiDotNetCore always executes the resource definition that matches the *stored* type. +This enables to implement business logic in a central place, irrespective of which endpoint was used or whether base types were used in relationships. + +To delegate logic for base types to their matching resource type, you can build a chain of resource definitions. And because you'll always get the +actually stored types (for relationships too), you can type-check left-side and right-side types in resources definitions. + +```c# +public sealed class HumanDefinition : JsonApiResourceDefinition +{ + public HumanDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) + { + } + + public override Task OnSetToOneRelationshipAsync(Human leftResource, + HasOneAttribute hasOneRelationship, IIdentifiable? rightResourceId, + WriteOperationKind writeOperation, CancellationToken cancellationToken) + { + if (leftResource is Man && + hasOneRelationship.Property.Name == nameof(Human.BestFriend) && + rightResourceId is Woman) + { + throw new Exception("Men are not supposed to have a female best friend."); + } + + return Task.FromResult(rightResourceId); + } + + public override Task OnWritingAsync(Human resource, WriteOperationKind writeOperation, + CancellationToken cancellationToken) + { + if (writeOperation is WriteOperationKind.CreateResource or + WriteOperationKind.UpdateResource) + { + if (resource is Man { HasBeard: true }) + { + throw new Exception("Only shaved men, please."); + } + } + + return Task.CompletedTask; + } +} + +public sealed class WomanDefinition : JsonApiResourceDefinition +{ + private readonly IResourceDefinition _baseDefinition; + + public WomanDefinition(IResourceGraph resourceGraph, + IResourceDefinition baseDefinition) + : base(resourceGraph) + { + _baseDefinition = baseDefinition; + } + + public override Task OnSetToOneRelationshipAsync(Woman leftResource, + HasOneAttribute hasOneRelationship, IIdentifiable? rightResourceId, + WriteOperationKind writeOperation, CancellationToken cancellationToken) + { + if (ResourceType.BaseType!.FindRelationshipByPublicName( + hasOneRelationship.PublicName) != null) + { + // Delegate to resource definition for base type Human. + return _baseDefinition.OnSetToOneRelationshipAsync(leftResource, + hasOneRelationship, rightResourceId, writeOperation, cancellationToken); + } + + // Handle here. + if (hasOneRelationship.Property.Name == nameof(Woman.Husband) && + rightResourceId == null) + { + throw new Exception("We don't accept unmarried women at this time."); + } + + return Task.FromResult(rightResourceId); + } + + public override async Task OnPrepareWriteAsync(Woman resource, + WriteOperationKind writeOperation, CancellationToken cancellationToken) + { + // Run rules in resource definition for base type Human. + await _baseDefinition.OnPrepareWriteAsync(resource, writeOperation, cancellationToken); + + // Run rules for type Woman. + if (resource.MaidenName == null) + { + throw new Exception("Women should have a maiden name."); + } + } +} + +public sealed class ManDefinition : JsonApiResourceDefinition +{ + private readonly IResourceDefinition _baseDefinition; + + public ManDefinition(IResourceGraph resourceGraph, + IResourceDefinition baseDefinition) + : base(resourceGraph) + { + _baseDefinition = baseDefinition; + } + + public override Task OnSetToOneRelationshipAsync(Man leftResource, + HasOneAttribute hasOneRelationship, IIdentifiable? rightResourceId, + WriteOperationKind writeOperation, CancellationToken cancellationToken) + { + // No man-specific logic, but we'll still need to delegate. + return _baseDefinition.OnSetToOneRelationshipAsync(leftResource, hasOneRelationship, + rightResourceId, writeOperation, cancellationToken); + } + + public override Task OnWritingAsync(Man resource, WriteOperationKind writeOperation, + CancellationToken cancellationToken) + { + // No man-specific logic, but we'll still need to delegate. + return _baseDefinition.OnWritingAsync(resource, writeOperation, cancellationToken); + } +} +``` diff --git a/docs/usage/toc.md b/docs/usage/toc.md index fabef61b68..87542e748a 100644 --- a/docs/usage/toc.md +++ b/docs/usage/toc.md @@ -1,6 +1,7 @@ # [Resources](resources/index.md) ## [Attributes](resources/attributes.md) ## [Relationships](resources/relationships.md) +## [Inheritance](resources/inheritance.md) ## [Nullability](resources/nullability.md) # Reading data diff --git a/src/Examples/DatabasePerTenantExample/Controllers/EmployeesController.cs b/src/Examples/DatabasePerTenantExample/Controllers/EmployeesController.cs new file mode 100644 index 0000000000..5e17afab9b --- /dev/null +++ b/src/Examples/DatabasePerTenantExample/Controllers/EmployeesController.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.Controllers.Annotations; +using Microsoft.AspNetCore.Mvc; + +namespace DatabasePerTenantExample.Controllers; + +// Workaround for https://youtrack.jetbrains.com/issue/RSRP-487028 +public partial class EmployeesController +{ +} + +[DisableRoutingConvention] +[Route("api/{tenantName}/employees")] +partial class EmployeesController +{ +} diff --git a/src/Examples/DatabasePerTenantExample/Data/AppDbContext.cs b/src/Examples/DatabasePerTenantExample/Data/AppDbContext.cs new file mode 100644 index 0000000000..c70fc8655f --- /dev/null +++ b/src/Examples/DatabasePerTenantExample/Data/AppDbContext.cs @@ -0,0 +1,80 @@ +using System.Net; +using DatabasePerTenantExample.Models; +using JetBrains.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.EntityFrameworkCore; + +namespace DatabasePerTenantExample.Data; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public sealed class AppDbContext : DbContext +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IConfiguration _configuration; + private string? _forcedTenantName; + + public DbSet Employees => Set(); + + public AppDbContext(IHttpContextAccessor httpContextAccessor, IConfiguration configuration) + { + _httpContextAccessor = httpContextAccessor; + _configuration = configuration; + } + + public void SetTenantName(string tenantName) + { + _forcedTenantName = tenantName; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + string connectionString = GetConnectionString(); + optionsBuilder.UseNpgsql(connectionString); + } + + private string GetConnectionString() + { + string? tenantName = GetTenantName(); + string connectionString = _configuration[$"Data:{tenantName ?? "Default"}Connection"]; + + if (connectionString == null) + { + throw GetErrorForInvalidTenant(tenantName); + } + + string postgresPassword = Environment.GetEnvironmentVariable("PGPASSWORD") ?? "postgres"; + return connectionString.Replace("###", postgresPassword); + } + + private string? GetTenantName() + { + if (_forcedTenantName != null) + { + return _forcedTenantName; + } + + if (_httpContextAccessor.HttpContext != null) + { + string? tenantName = (string?)_httpContextAccessor.HttpContext.Request.RouteValues["tenantName"]; + + if (tenantName == null) + { + throw GetErrorForInvalidTenant(null); + } + + return tenantName; + } + + return null; + } + + private static JsonApiException GetErrorForInvalidTenant(string? tenantName) + { + return new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) + { + Title = "Missing or invalid tenant in URL.", + Detail = $"Tenant '{tenantName}' does not exist." + }); + } +} diff --git a/src/Examples/DatabasePerTenantExample/DatabasePerTenantExample.csproj b/src/Examples/DatabasePerTenantExample/DatabasePerTenantExample.csproj new file mode 100644 index 0000000000..b243e99ec2 --- /dev/null +++ b/src/Examples/DatabasePerTenantExample/DatabasePerTenantExample.csproj @@ -0,0 +1,16 @@ + + + $(TargetFrameworkName) + + + + + + + + + + + + diff --git a/src/Examples/DatabasePerTenantExample/Models/Employee.cs b/src/Examples/DatabasePerTenantExample/Models/Employee.cs new file mode 100644 index 0000000000..cc79449880 --- /dev/null +++ b/src/Examples/DatabasePerTenantExample/Models/Employee.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace DatabasePerTenantExample.Models; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource] +public sealed class Employee : Identifiable +{ + [Attr] + public string FirstName { get; set; } = null!; + + [Attr] + public string LastName { get; set; } = null!; + + [Attr] + public string CompanyName { get; set; } = null!; +} diff --git a/src/Examples/DatabasePerTenantExample/Program.cs b/src/Examples/DatabasePerTenantExample/Program.cs new file mode 100644 index 0000000000..b6f960831d --- /dev/null +++ b/src/Examples/DatabasePerTenantExample/Program.cs @@ -0,0 +1,58 @@ +using DatabasePerTenantExample.Data; +using DatabasePerTenantExample.Models; +using JsonApiDotNetCore.Configuration; +using Microsoft.EntityFrameworkCore; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddSingleton(); +builder.Services.AddDbContext(options => options.UseNpgsql()); + +builder.Services.AddJsonApi(options => +{ + options.Namespace = "api"; + options.UseRelativeLinks = true; + options.SerializerOptions.WriteIndented = true; +}); + +WebApplication app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.UseRouting(); +app.UseJsonApi(); +app.MapControllers(); + +await CreateDatabaseAsync(null, app.Services); +await CreateDatabaseAsync("AdventureWorks", app.Services); +await CreateDatabaseAsync("Contoso", app.Services); + +app.Run(); + +static async Task CreateDatabaseAsync(string? tenantName, IServiceProvider serviceProvider) +{ + await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + if (tenantName != null) + { + dbContext.SetTenantName(tenantName); + } + + await dbContext.Database.EnsureDeletedAsync(); + await dbContext.Database.EnsureCreatedAsync(); + + if (tenantName != null) + { + dbContext.Employees.Add(new Employee + { + FirstName = "John", + LastName = "Doe", + CompanyName = tenantName + }); + + await dbContext.SaveChangesAsync(); + } +} diff --git a/src/Examples/DatabasePerTenantExample/Properties/launchSettings.json b/src/Examples/DatabasePerTenantExample/Properties/launchSettings.json new file mode 100644 index 0000000000..1ab75296f7 --- /dev/null +++ b/src/Examples/DatabasePerTenantExample/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14147", + "sslPort": 44340 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/AdventureWorks/employees", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Kestrel": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/AdventureWorks/employees", + "applicationUrl": "https://localhost:44347;http://localhost:14147", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Examples/DatabasePerTenantExample/appsettings.json b/src/Examples/DatabasePerTenantExample/appsettings.json new file mode 100644 index 0000000000..c065f66c64 --- /dev/null +++ b/src/Examples/DatabasePerTenantExample/appsettings.json @@ -0,0 +1,15 @@ +{ + "Data": { + "DefaultConnection": "Host=localhost;Port=5432;Database=DefaultTenantDb;User ID=postgres;Password=###", + "AdventureWorksConnection": "Host=localhost;Port=5432;Database=AdventureWorks;User ID=postgres;Password=###", + "ContosoConnection": "Host=localhost;Port=5432;Database=Contoso;User ID=postgres;Password=###" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore.Database.Command": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Program.cs b/src/Examples/JsonApiDotNetCoreExample/Program.cs index 3eb8de8fef..0bd2bd23be 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Program.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Program.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Diagnostics; @@ -7,6 +8,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection.Extensions; +[assembly: ExcludeFromCodeCoverage] + WebApplication app = CreateWebApplication(args); await CreateDatabaseAsync(app.Services); diff --git a/src/JsonApiDotNetCore.Annotations/ArgumentGuard.cs b/src/JsonApiDotNetCore.Annotations/ArgumentGuard.cs index 23b64d1044..be336e56a0 100644 --- a/src/JsonApiDotNetCore.Annotations/ArgumentGuard.cs +++ b/src/JsonApiDotNetCore.Annotations/ArgumentGuard.cs @@ -18,13 +18,24 @@ public static void NotNull([NoEnumeration] [SysNotNull] T? value, [InvokerPar } [AssertionMethod] - public static void NotNullNorEmpty([SysNotNull] IEnumerable? value, [InvokerParameterName] string name, string? collectionName = null) + public static void NotNullNorEmpty([SysNotNull] IEnumerable? value, [InvokerParameterName] string name) { NotNull(value, name); if (!value.Any()) { - throw new ArgumentException($"Must have one or more {collectionName ?? name}.", name); + throw new ArgumentException($"Must have one or more {name}.", name); + } + } + + [AssertionMethod] + public static void NotNullNorEmpty([SysNotNull] IEnumerable? value, [InvokerParameterName] string name, string collectionName) + { + NotNull(value, name); + + if (!value.Any()) + { + throw new ArgumentException($"Must have one or more {collectionName}.", name); } } diff --git a/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs b/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs index 88037e66e3..a308607c3b 100644 --- a/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs +++ b/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs @@ -66,9 +66,19 @@ private Type ToConcreteCollectionType(Type collectionType) /// /// Returns a collection that contains zero, one or multiple resources, depending on the specified value. /// - public ICollection ExtractResources(object? value) + public IReadOnlyCollection ExtractResources(object? value) { - if (value is ICollection resourceCollection) + if (value is List resourceList) + { + return resourceList; + } + + if (value is HashSet resourceSet) + { + return resourceSet; + } + + if (value is IReadOnlyCollection resourceCollection) { return resourceCollection; } diff --git a/src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs b/src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs index ce7ccd1870..515dfe8a63 100644 --- a/src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs +++ b/src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs @@ -11,6 +11,7 @@ public sealed class ResourceType { private readonly Dictionary _fieldsByPublicName = new(); private readonly Dictionary _fieldsByPropertyName = new(); + private readonly Lazy> _lazyAllConcreteDerivedTypes; /// /// The publicly exposed resource name. @@ -28,22 +29,35 @@ public sealed class ResourceType public Type IdentityClrType { get; } /// - /// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields. + /// The base resource type, in case this is a derived type. + /// + public ResourceType? BaseType { get; internal set; } + + /// + /// The resource types that directly derive from this one. + /// + public IReadOnlySet DirectlyDerivedTypes { get; internal set; } = new HashSet(); + + /// + /// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields. When using resource inheritance, this + /// includes the attributes and relationships from base types. /// public IReadOnlyCollection Fields { get; } /// - /// Exposed resource attributes. See https://jsonapi.org/format/#document-resource-object-attributes. + /// Exposed resource attributes. See https://jsonapi.org/format/#document-resource-object-attributes. When using resource inheritance, this includes the + /// attributes from base types. /// public IReadOnlyCollection Attributes { get; } /// - /// Exposed resource relationships. See https://jsonapi.org/format/#document-resource-object-relationships. + /// Exposed resource relationships. See https://jsonapi.org/format/#document-resource-object-relationships. When using resource inheritance, this + /// includes the relationships from base types. /// public IReadOnlyCollection Relationships { get; } /// - /// Related entities that are not exposed as resource relationships. + /// Related entities that are not exposed as resource relationships. When using resource inheritance, this includes the eager-loads from base types. /// public IReadOnlyCollection EagerLoads { get; } @@ -75,8 +89,14 @@ public sealed class ResourceType /// public LinkTypes RelationshipLinks { get; } - public ResourceType(string publicName, Type clrType, Type identityClrType, IReadOnlyCollection? attributes = null, - IReadOnlyCollection? relationships = null, IReadOnlyCollection? eagerLoads = null, + public ResourceType(string publicName, Type clrType, Type identityClrType, LinkTypes topLevelLinks = LinkTypes.NotConfigured, + LinkTypes resourceLinks = LinkTypes.NotConfigured, LinkTypes relationshipLinks = LinkTypes.NotConfigured) + : this(publicName, clrType, identityClrType, null, null, null, topLevelLinks, resourceLinks, relationshipLinks) + { + } + + public ResourceType(string publicName, Type clrType, Type identityClrType, IReadOnlyCollection? attributes, + IReadOnlyCollection? relationships, IReadOnlyCollection? eagerLoads, LinkTypes topLevelLinks = LinkTypes.NotConfigured, LinkTypes resourceLinks = LinkTypes.NotConfigured, LinkTypes relationshipLinks = LinkTypes.NotConfigured) { @@ -100,6 +120,29 @@ public ResourceType(string publicName, Type clrType, Type identityClrType, IRead _fieldsByPublicName.Add(field.PublicName, field); _fieldsByPropertyName.Add(field.Property.Name, field); } + + _lazyAllConcreteDerivedTypes = new Lazy>(ResolveAllConcreteDerivedTypes, LazyThreadSafetyMode.PublicationOnly); + } + + private IReadOnlySet ResolveAllConcreteDerivedTypes() + { + var allConcreteDerivedTypes = new HashSet(); + AddConcreteDerivedTypes(this, allConcreteDerivedTypes); + + return allConcreteDerivedTypes; + } + + private static void AddConcreteDerivedTypes(ResourceType resourceType, ISet allConcreteDerivedTypes) + { + foreach (ResourceType derivedType in resourceType.DirectlyDerivedTypes) + { + if (!derivedType.ClrType.IsAbstract) + { + allConcreteDerivedTypes.Add(derivedType); + } + + AddConcreteDerivedTypes(derivedType, allConcreteDerivedTypes); + } } public AttrAttribute GetAttributeByPublicName(string publicName) @@ -161,6 +204,111 @@ public RelationshipAttribute GetRelationshipByPropertyName(string propertyName) : null; } + /// + /// Returns all directly and indirectly non-abstract resource types that derive from this resource type. + /// + public IReadOnlySet GetAllConcreteDerivedTypes() + { + return _lazyAllConcreteDerivedTypes.Value; + } + + /// + /// Searches the tree of derived types to find a match for the specified . + /// + public ResourceType GetTypeOrDerived(Type clrType) + { + ArgumentGuard.NotNull(clrType, nameof(clrType)); + + ResourceType? derivedType = FindTypeOrDerived(this, clrType); + + if (derivedType == null) + { + throw new InvalidOperationException($"Resource type '{PublicName}' is not a base type of '{clrType}'."); + } + + return derivedType; + } + + private static ResourceType? FindTypeOrDerived(ResourceType type, Type clrType) + { + if (type.ClrType == clrType) + { + return type; + } + + foreach (ResourceType derivedType in type.DirectlyDerivedTypes) + { + ResourceType? matchingType = FindTypeOrDerived(derivedType, clrType); + + if (matchingType != null) + { + return matchingType; + } + } + + return null; + } + + internal IReadOnlySet GetAttributesInTypeOrDerived(string publicName) + { + return GetAttributesInTypeOrDerived(this, publicName); + } + + private static IReadOnlySet GetAttributesInTypeOrDerived(ResourceType resourceType, string publicName) + { + AttrAttribute? attribute = resourceType.FindAttributeByPublicName(publicName); + + if (attribute != null) + { + return attribute.AsHashSet(); + } + + // Hiding base members using the 'new' keyword instead of 'override' (effectively breaking inheritance) is currently not supported. + // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/knowing-when-to-use-override-and-new-keywords + HashSet attributesInDerivedTypes = new(); + + foreach (AttrAttribute attributeInDerivedType in resourceType.DirectlyDerivedTypes + .Select(derivedType => GetAttributesInTypeOrDerived(derivedType, publicName)).SelectMany(attributesInDerivedType => attributesInDerivedType)) + { + attributesInDerivedTypes.Add(attributeInDerivedType); + } + + return attributesInDerivedTypes; + } + + internal IReadOnlySet GetRelationshipsInTypeOrDerived(string publicName) + { + return GetRelationshipsInTypeOrDerived(this, publicName); + } + + private static IReadOnlySet GetRelationshipsInTypeOrDerived(ResourceType resourceType, string publicName) + { + RelationshipAttribute? relationship = resourceType.FindRelationshipByPublicName(publicName); + + if (relationship != null) + { + return relationship.AsHashSet(); + } + + // Hiding base members using the 'new' keyword instead of 'override' (effectively breaking inheritance) is currently not supported. + // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/knowing-when-to-use-override-and-new-keywords + HashSet relationshipsInDerivedTypes = new(); + + foreach (RelationshipAttribute relationshipInDerivedType in resourceType.DirectlyDerivedTypes + .Select(derivedType => GetRelationshipsInTypeOrDerived(derivedType, publicName)) + .SelectMany(relationshipsInDerivedType => relationshipsInDerivedType)) + { + relationshipsInDerivedTypes.Add(relationshipInDerivedType); + } + + return relationshipsInDerivedTypes; + } + + internal bool IsPartOfTypeHierarchy() + { + return BaseType != null || DirectlyDerivedTypes.Any(); + } + public override string ToString() { return PublicName; diff --git a/src/JsonApiDotNetCore.Annotations/ObjectExtensions.cs b/src/JsonApiDotNetCore.Annotations/ObjectExtensions.cs index bd42993803..b7fd934fbe 100644 --- a/src/JsonApiDotNetCore.Annotations/ObjectExtensions.cs +++ b/src/JsonApiDotNetCore.Annotations/ObjectExtensions.cs @@ -1,4 +1,4 @@ -#pragma warning disable AV1130 // Return type in method signature should be a collection interface instead of a concrete type +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection namespace JsonApiDotNetCore; diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs index 6406ba17ff..11320a7abc 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs @@ -14,21 +14,8 @@ public abstract class RelationshipAttribute : ResourceFieldAttribute { private protected static readonly CollectionConverter CollectionConverter = new(); - /// - /// The CLR type in which this relationship is declared. - /// - internal Type? LeftClrType { get; set; } - - /// - /// The CLR type this relationship points to. In the case of a relationship, this value will be the collection element - /// type. - /// - /// - /// Tags { get; set; } // RightClrType: typeof(Tag) - /// ]]> - /// - internal Type? RightClrType { get; set; } + // These are definitely assigned after building the resource graph, which is why their public equivalents are declared as non-nullable. + private ResourceType? _rightType; /// /// The of the Entity Framework Core inverse navigation, which may or may not exist. Even if it exists, it may not be exposed @@ -52,15 +39,28 @@ public abstract class RelationshipAttribute : ResourceFieldAttribute public PropertyInfo? InverseNavigationProperty { get; set; } /// - /// The containing resource type in which this relationship is declared. + /// The containing resource type in which this relationship is declared. Identical to . /// - public ResourceType LeftType { get; internal set; } = null!; + public ResourceType LeftType => Type; /// /// The resource type this relationship points to. In the case of a relationship, this value will be the collection /// element type. /// - public ResourceType RightType { get; internal set; } = null!; + /// + /// Tags { get; set; } // RightType: Tag + /// ]]> + /// + public ResourceType RightType + { + get => _rightType!; + internal set + { + ArgumentGuard.NotNull(value, nameof(value)); + _rightType = value; + } + } /// /// Configures which links to write in the relationship-level links object for this relationship. Defaults to , @@ -91,12 +91,11 @@ public override bool Equals(object? obj) var other = (RelationshipAttribute)obj; - return LeftClrType == other.LeftClrType && RightClrType == other.RightClrType && Links == other.Links && CanInclude == other.CanInclude && - base.Equals(other); + return _rightType?.ClrType == other._rightType?.ClrType && Links == other.Links && CanInclude == other.CanInclude && base.Equals(other); } public override int GetHashCode() { - return HashCode.Combine(LeftClrType, RightClrType, Links, CanInclude, base.GetHashCode()); + return HashCode.Combine(_rightType?.ClrType, Links, CanInclude, base.GetHashCode()); } } diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceFieldAttribute.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceFieldAttribute.cs index c7a44f59c8..599b17a42a 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceFieldAttribute.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceFieldAttribute.cs @@ -1,5 +1,6 @@ using System.Reflection; using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; // ReSharper disable NonReadonlyMemberInGetHashCode @@ -15,6 +16,7 @@ public abstract class ResourceFieldAttribute : Attribute // These are definitely assigned after building the resource graph, which is why their public equivalents are declared as non-nullable. private string? _publicName; private PropertyInfo? _property; + private ResourceType? _type; /// /// The publicly exposed name of this JSON:API field. When not explicitly assigned, the configured naming convention is applied on the property name. @@ -46,6 +48,19 @@ internal set } } + /// + /// The containing resource type in which this field is declared. + /// + public ResourceType Type + { + get => _type!; + internal set + { + ArgumentGuard.NotNull(value, nameof(value)); + _type = value; + } + } + /// /// Gets the value of this field on the specified resource instance. Throws if the property is write-only or if the field does not belong to the /// specified resource instance. diff --git a/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj index 5b48e853cb..22bbc3665a 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj +++ b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj @@ -28,7 +28,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs index 4330a6c21c..dca0b5aef7 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs @@ -72,7 +72,7 @@ private static IEnumerable AddJsonApiMetadataToAction(ActionDe } case PrimaryRequestMetadata primaryMetadata: { - UpdateBodyParameterDescriptor(endpoint, primaryMetadata.DocumentType); + UpdateBodyParameterDescriptor(endpoint, primaryMetadata.DocumentType, null); return Array.Empty(); } case NonPrimaryEndpointMetadata nonPrimaryEndpointMetadata and (RelationshipResponseMetadata or SecondaryResponseMetadata): @@ -140,7 +140,7 @@ private static IEnumerable Expand(ActionDescriptor genericEndp return expansion; } - private static void UpdateBodyParameterDescriptor(ActionDescriptor endpoint, Type documentType, string? parameterName = null) + private static void UpdateBodyParameterDescriptor(ActionDescriptor endpoint, Type documentType, string? parameterName) { ControllerParameterDescriptor? requestBodyDescriptor = endpoint.GetBodyParameterDescriptor(); diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj b/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj index f3575b9189..95e9b33f7b 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj @@ -32,7 +32,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/CachingSwaggerGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/CachingSwaggerGenerator.cs index eded09c490..26f5fab3f8 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/CachingSwaggerGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/CachingSwaggerGenerator.cs @@ -3,6 +3,8 @@ using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; +#pragma warning disable AV1553 // Do not use optional parameters with default value null for strings, collections or tasks + namespace JsonApiDotNetCore.OpenApi.SwaggerComponents; /// diff --git a/src/JsonApiDotNetCore/ArrayFactory.cs b/src/JsonApiDotNetCore/ArrayFactory.cs index 2969f34d17..6ad678c64d 100644 --- a/src/JsonApiDotNetCore/ArrayFactory.cs +++ b/src/JsonApiDotNetCore/ArrayFactory.cs @@ -1,5 +1,5 @@ #pragma warning disable AV1008 // Class should not be static -#pragma warning disable AV1130 // Return type in method signature should be a collection interface instead of a concrete type +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection namespace JsonApiDotNetCore; diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs index cbbc702d0c..9cb463ab10 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs @@ -96,7 +96,7 @@ private void AssertLocalIdIsAssigned(IIdentifiable resource) { if (resource.LocalId != null) { - ResourceType resourceType = _resourceGraph.GetResourceType(resource.GetType()); + ResourceType resourceType = _resourceGraph.GetResourceType(resource.GetClrType()); _localIdTracker.GetValue(resource.LocalId, resourceType); } } diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs index 4a26e36710..6524252abf 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs @@ -140,7 +140,7 @@ private void AssignStringId(IIdentifiable resource) { if (resource.LocalId != null) { - ResourceType resourceType = _resourceGraph.GetResourceType(resource.GetType()); + ResourceType resourceType = _resourceGraph.GetResourceType(resource.GetClrType()); resource.StringId = _localIdTracker.GetValue(resource.LocalId, resourceType); } } diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs index 1f15ea293f..5eb09ccbc3 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs @@ -40,7 +40,7 @@ public SetRelationshipProcessor(ISetRelationshipService service) if (relationship is HasManyAttribute) { - ICollection rightResources = _collectionConverter.ExtractResources(rightValue); + IReadOnlyCollection rightResources = _collectionConverter.ExtractResources(rightValue); return rightResources.ToHashSet(IdentifiableComparer.Instance); } diff --git a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs index 9ccce6c60a..a98b74c3fe 100644 --- a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs @@ -54,7 +54,7 @@ ResourceType GetResourceType() /// (TResource resource) => new { resource.Attribute1, resource.Relationship2 } /// ]]> /// - IReadOnlyCollection GetFields(Expression> selector) + IReadOnlyCollection GetFields(Expression> selector) where TResource : class, IIdentifiable; /// @@ -68,7 +68,7 @@ IReadOnlyCollection GetFields(Expression new { resource.attribute1, resource.Attribute2 } /// ]]> /// - IReadOnlyCollection GetAttributes(Expression> selector) + IReadOnlyCollection GetAttributes(Expression> selector) where TResource : class, IIdentifiable; /// @@ -82,6 +82,6 @@ IReadOnlyCollection GetAttributes(Expression new { resource.Relationship1, resource.Relationship2 } /// ]]> /// - IReadOnlyCollection GetRelationships(Expression> selector) + IReadOnlyCollection GetRelationships(Expression> selector) where TResource : class, IIdentifiable; } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index aa7ed0d434..eda6374acf 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -102,6 +102,12 @@ public sealed class JsonApiOptions : IJsonApiOptions } }; + static JsonApiOptions() + { + // Bug workaround for https://github.com/dotnet/efcore/issues/27436 + AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue26779", true); + } + public JsonApiOptions() { _lazySerializerReadOptions = new Lazy(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.PublicationOnly); diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs index d5acc8a1f9..d693fa2c3c 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs @@ -42,7 +42,7 @@ public ResourceType GetResourceType(string publicName) if (resourceType == null) { - throw new InvalidOperationException($"Resource type '{publicName}' does not exist."); + throw new InvalidOperationException($"Resource type '{publicName}' does not exist in the resource graph."); } return resourceType; @@ -63,7 +63,7 @@ public ResourceType GetResourceType(Type resourceClrType) if (resourceType == null) { - throw new InvalidOperationException($"Resource of type '{resourceClrType.Name}' does not exist."); + throw new InvalidOperationException($"Type '{resourceClrType}' does not exist in the resource graph."); } return resourceType; @@ -91,7 +91,7 @@ public ResourceType GetResourceType() } /// - public IReadOnlyCollection GetFields(Expression> selector) + public IReadOnlyCollection GetFields(Expression> selector) where TResource : class, IIdentifiable { ArgumentGuard.NotNull(selector, nameof(selector)); @@ -100,7 +100,7 @@ public IReadOnlyCollection GetFields(Expressi } /// - public IReadOnlyCollection GetAttributes(Expression> selector) + public IReadOnlyCollection GetAttributes(Expression> selector) where TResource : class, IIdentifiable { ArgumentGuard.NotNull(selector, nameof(selector)); @@ -109,7 +109,7 @@ public IReadOnlyCollection GetAttributes(Expression - public IReadOnlyCollection GetRelationships(Expression> selector) + public IReadOnlyCollection GetRelationships(Expression> selector) where TResource : class, IIdentifiable { ArgumentGuard.NotNull(selector, nameof(selector)); @@ -117,7 +117,7 @@ public IReadOnlyCollection GetRelationships(Ex return FilterFields(selector); } - private IReadOnlyCollection FilterFields(Expression> selector) + private IReadOnlyCollection FilterFields(Expression> selector) where TResource : class, IIdentifiable where TField : ResourceFieldAttribute { @@ -157,7 +157,7 @@ private IReadOnlyCollection GetFieldsOfType() return (IReadOnlyCollection)resourceType.Fields; } - private IEnumerable ToMemberNames(Expression> selector) + private IEnumerable ToMemberNames(Expression> selector) { Expression selectorBody = RemoveConvert(selector.Body); diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 1352160f11..e07318c7f7 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -43,21 +43,103 @@ public IResourceGraph Build() var resourceGraph = new ResourceGraph(resourceTypes); - foreach (RelationshipAttribute relationship in resourceTypes.SelectMany(resourceType => resourceType.Relationships)) + SetFieldTypes(resourceGraph); + SetRelationshipTypes(resourceGraph); + SetDirectlyDerivedTypes(resourceGraph); + ValidateFieldsInDerivedTypes(resourceGraph); + + return resourceGraph; + } + + private static void SetFieldTypes(ResourceGraph resourceGraph) + { + foreach (ResourceFieldAttribute field in resourceGraph.GetResourceTypes().SelectMany(resourceType => resourceType.Fields)) { - relationship.LeftType = resourceGraph.GetResourceType(relationship.LeftClrType!); - ResourceType? rightType = resourceGraph.FindResourceType(relationship.RightClrType!); + field.Type = resourceGraph.GetResourceType(field.Property.ReflectedType!); + } + } + + private static void SetRelationshipTypes(ResourceGraph resourceGraph) + { + foreach (RelationshipAttribute relationship in resourceGraph.GetResourceTypes().SelectMany(resourceType => resourceType.Relationships)) + { + Type rightClrType = relationship is HasOneAttribute + ? relationship.Property.PropertyType + : relationship.Property.PropertyType.GetGenericArguments()[0]; + + ResourceType? rightType = resourceGraph.FindResourceType(rightClrType); if (rightType == null) { - throw new InvalidConfigurationException($"Resource type '{relationship.LeftClrType}' depends on " + - $"'{relationship.RightClrType}', which was not added to the resource graph."); + throw new InvalidConfigurationException($"Resource type '{relationship.LeftType.ClrType}' depends on " + + $"'{rightClrType}', which was not added to the resource graph."); } relationship.RightType = rightType; } + } - return resourceGraph; + private static void SetDirectlyDerivedTypes(ResourceGraph resourceGraph) + { + Dictionary> directlyDerivedTypesPerBaseType = new(); + + foreach (ResourceType resourceType in resourceGraph.GetResourceTypes()) + { + ResourceType? baseType = resourceGraph.FindResourceType(resourceType.ClrType.BaseType!); + + if (baseType != null) + { + resourceType.BaseType = baseType; + + if (!directlyDerivedTypesPerBaseType.ContainsKey(baseType)) + { + directlyDerivedTypesPerBaseType[baseType] = new HashSet(); + } + + directlyDerivedTypesPerBaseType[baseType].Add(resourceType); + } + } + + foreach ((ResourceType baseType, HashSet directlyDerivedTypes) in directlyDerivedTypesPerBaseType) + { + baseType.DirectlyDerivedTypes = directlyDerivedTypes; + } + } + + private void ValidateFieldsInDerivedTypes(ResourceGraph resourceGraph) + { + foreach (ResourceType resourceType in resourceGraph.GetResourceTypes()) + { + if (resourceType.BaseType != null) + { + ValidateAttributesInDerivedType(resourceType); + ValidateRelationshipsInDerivedType(resourceType); + } + } + } + + private static void ValidateAttributesInDerivedType(ResourceType resourceType) + { + foreach (AttrAttribute attribute in resourceType.BaseType!.Attributes) + { + if (resourceType.FindAttributeByPublicName(attribute.PublicName) == null) + { + throw new InvalidConfigurationException($"Attribute '{attribute.PublicName}' from base type " + + $"'{resourceType.BaseType.ClrType}' does not exist in derived type '{resourceType.ClrType}'."); + } + } + } + + private static void ValidateRelationshipsInDerivedType(ResourceType resourceType) + { + foreach (RelationshipAttribute relationship in resourceType.BaseType!.Relationships) + { + if (resourceType.FindRelationshipByPublicName(relationship.PublicName) == null) + { + throw new InvalidConfigurationException($"Relationship '{relationship.PublicName}' from base type " + + $"'{resourceType.BaseType.ClrType}' does not exist in derived type '{resourceType.ClrType}'."); + } + } } public ResourceGraphBuilder Add(DbContext dbContext) @@ -93,8 +175,10 @@ private static bool IsImplicitManyToManyJoinEntity(IEntityType entityType) /// The name under which the resource is publicly exposed by the API. If nothing is specified, the naming convention is applied on the pluralized CLR /// type name. /// +#pragma warning disable AV1553 // Do not use optional parameters with default value null for strings, collections or tasks public ResourceGraphBuilder Add(string? publicName = null) where TResource : class, IIdentifiable +#pragma warning restore AV1553 // Do not use optional parameters with default value null for strings, collections or tasks { return Add(typeof(TResource), typeof(TId), publicName); } @@ -112,7 +196,9 @@ public ResourceGraphBuilder Add(string? publicName = null) /// The name under which the resource is publicly exposed by the API. If nothing is specified, the naming convention is applied on the pluralized CLR /// type name. /// +#pragma warning disable AV1553 // Do not use optional parameters with default value null for strings, collections or tasks public ResourceGraphBuilder Add(Type resourceClrType, Type? idClrType = null, string? publicName = null) +#pragma warning restore AV1553 // Do not use optional parameters with default value null for strings, collections or tasks { ArgumentGuard.NotNull(resourceClrType, nameof(resourceClrType)); @@ -221,8 +307,6 @@ private IReadOnlyCollection GetRelationships(Type resourc { relationship.Property = property; SetPublicName(relationship, property); - relationship.LeftClrType = resourceClrType; - relationship.RightClrType = GetRelationshipType(relationship, property); IncludeField(relationshipsByName, relationship); } @@ -237,14 +321,6 @@ private void SetPublicName(ResourceFieldAttribute field, PropertyInfo property) field.PublicName ??= FormatPropertyName(property); } - private Type GetRelationshipType(RelationshipAttribute relationship, PropertyInfo property) - { - ArgumentGuard.NotNull(relationship, nameof(relationship)); - ArgumentGuard.NotNull(property, nameof(property)); - - return relationship is HasOneAttribute ? property.PropertyType : property.PropertyType.GetGenericArguments()[0]; - } - private IReadOnlyCollection GetEagerLoads(Type resourceClrType, int recursionDepth = 0) { AssertNoInfiniteRecursion(recursionDepth); diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index 64a15e2eda..8e5d15a7c4 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -6,6 +6,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection + namespace JsonApiDotNetCore.Configuration; [PublicAPI] @@ -16,9 +18,11 @@ public static class ServiceCollectionExtensions /// /// Configures JsonApiDotNetCore by registering resources manually. /// +#pragma warning disable AV1553 // Do not use optional parameters with default value null for strings, collections or tasks public static IServiceCollection AddJsonApi(this IServiceCollection services, Action? options = null, Action? discovery = null, Action? resources = null, IMvcCoreBuilder? mvcBuilder = null, ICollection? dbContextTypes = null) +#pragma warning restore AV1553 // Do not use optional parameters with default value null for strings, collections or tasks { ArgumentGuard.NotNull(services, nameof(services)); @@ -111,7 +115,7 @@ private static void RegisterTypeForUnboundInterfaces(IServiceCollection serviceC if (!seenCompatibleInterface) { - throw new InvalidConfigurationException($"{implementationType} does not implement any of the expected JsonApiDotNetCore interfaces."); + throw new InvalidConfigurationException($"Type '{implementationType}' does not implement any of the expected JsonApiDotNetCore interfaces."); } } diff --git a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs index 2a0369d940..2f004ffdf1 100644 --- a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs @@ -89,7 +89,7 @@ internal sealed class TypeLocator private static (Type implementationType, Type serviceInterface)? GetContainerRegistrationFromType(Type nextType, Type unboundInterface, Type[] interfaceTypeArguments) { - if (!nextType.IsNested) + if (!nextType.IsNested && !nextType.IsAbstract && !nextType.IsInterface) { foreach (Type nextConstructedInterface in nextType.GetInterfaces().Where(type => type.IsGenericType)) { diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs index debcfa5c6f..a948d67edc 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs @@ -148,7 +148,8 @@ protected virtual void ValidateModelState(IList operations) Dictionary modelStateDictionary = requestModelState.ToDictionary(tuple => tuple.key, tuple => tuple.entry); throw new InvalidModelStateException(modelStateDictionary, typeof(IList), _options.IncludeExceptionStackTraceInErrors, - _resourceGraph, (collectionType, index) => collectionType == typeof(IList) ? operations[index].Resource.GetType() : null); + _resourceGraph, + (collectionType, index) => collectionType == typeof(IList) ? operations[index].Resource.GetClrType() : null); } } diff --git a/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs index fde552ec8f..44cc2955d9 100644 --- a/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs +++ b/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs @@ -32,7 +32,13 @@ static CascadingCodeTimer() } /// - public IDisposable Measure(string name, bool excludeInRelativeCost = false) + public IDisposable Measure(string name) + { + return Measure(name, false); + } + + /// + public IDisposable Measure(string name, bool excludeInRelativeCost) { MeasureScope childScope = CreateChildScope(name, excludeInRelativeCost); _activeScopeStack.Push(childScope); diff --git a/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs index 928017b7e0..a50c25eaba 100644 --- a/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs +++ b/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs @@ -11,7 +11,12 @@ private DisabledCodeTimer() { } - public IDisposable Measure(string name, bool excludeInRelativeCost = false) + public IDisposable Measure(string name) + { + return this; + } + + public IDisposable Measure(string name, bool excludeInRelativeCost) { return this; } diff --git a/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs index 23ff4ac605..ff219b5a37 100644 --- a/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs +++ b/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs @@ -5,6 +5,15 @@ namespace JsonApiDotNetCore.Diagnostics; /// public interface ICodeTimer : IDisposable { + /// + /// Starts recording the duration of a code block, while including this measurement in calculated percentages. Wrap this call in a using + /// statement, so the recording stops when the return value goes out of scope. + /// + /// + /// Description of what is being recorded. + /// + IDisposable Measure(string name); + /// /// Starts recording the duration of a code block. Wrap this call in a using statement, so the recording stops when the return value goes out of /// scope. @@ -13,9 +22,9 @@ public interface ICodeTimer : IDisposable /// Description of what is being recorded. /// /// - /// When set, indicates to exclude this measurement in calculated percentages. false by default. + /// When set, indicates to exclude this measurement in calculated percentages. /// - IDisposable Measure(string name, bool excludeInRelativeCost = false); + IDisposable Measure(string name, bool excludeInRelativeCost); /// /// Returns intermediate or final results. diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index cc91d0f324..e401db38fa 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -39,8 +39,8 @@ - - + + diff --git a/src/JsonApiDotNetCore/Queries/Expressions/AnyExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/AnyExpression.cs index be8a22abee..980a7846bc 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/AnyExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/AnyExpression.cs @@ -34,14 +34,24 @@ public override TResult Accept(QueryExpressionVisitor constant.ToString()).OrderBy(value => value))); + builder.Append(string.Join(",", Constants.Select(constant => toFullString ? constant.ToFullString() : constant.ToString()).OrderBy(value => value))); builder.Append(')'); return builder.ToString(); diff --git a/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs index 4c858bd743..9bf1c3bde8 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs @@ -33,6 +33,11 @@ public override string ToString() return $"{Operator.ToString().Camelize()}({Left},{Right})"; } + public override string ToFullString() + { + return $"{Operator.ToString().Camelize()}({Left.ToFullString()},{Right.ToFullString()})"; + } + public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs index 1bab96eaab..5de89ead7c 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs @@ -28,6 +28,11 @@ public override string ToString() return $"{Keywords.Count}({TargetCollection})"; } + public override string ToFullString() + { + return $"{Keywords.Count}({TargetCollection.ToFullString()})"; + } + public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/HasExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/HasExpression.cs index d1376a3091..c5387106d6 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/HasExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/HasExpression.cs @@ -27,16 +27,26 @@ public override TResult Accept(QueryExpressionVisitor GetRelationshipChains(I return converter.Chains; } - /// - /// Converts a set of relationship chains into a tree of inclusions. - /// - /// - /// Input chains: Blog, - /// Article -> Revisions -> Author - /// ]]> Output tree: - /// - /// - public IncludeExpression FromRelationshipChains(IEnumerable chains) - { - ArgumentGuard.NotNull(chains, nameof(chains)); - - IImmutableSet elements = ConvertChainsToElements(chains); - return elements.Any() ? new IncludeExpression(elements) : IncludeExpression.Empty; - } - - private static IImmutableSet ConvertChainsToElements(IEnumerable chains) - { - var rootNode = new MutableIncludeNode(null!); - - foreach (ResourceFieldChainExpression chain in chains) - { - ConvertChainToElement(chain, rootNode); - } - - return rootNode.Children.Values.Select(child => child.ToExpression()).ToImmutableHashSet(); - } - - private static void ConvertChainToElement(ResourceFieldChainExpression chain, MutableIncludeNode rootNode) - { - MutableIncludeNode currentNode = rootNode; - - foreach (RelationshipAttribute relationship in chain.Fields.OfType()) - { - if (!currentNode.Children.ContainsKey(relationship)) - { - currentNode.Children[relationship] = new MutableIncludeNode(relationship); - } - - currentNode = currentNode.Children[relationship]; - } - } - private sealed class IncludeToChainsConverter : QueryExpressionVisitor { private readonly Stack _parentRelationshipStack = new(); @@ -144,22 +90,4 @@ private void FlushChain(IncludeElementExpression expression) Chains.Add(new ResourceFieldChainExpression(chainBuilder.ToImmutable())); } } - - private sealed class MutableIncludeNode - { - private readonly RelationshipAttribute _relationship; - - public IDictionary Children { get; } = new Dictionary(); - - public MutableIncludeNode(RelationshipAttribute relationship) - { - _relationship = relationship; - } - - public IncludeElementExpression ToExpression() - { - IImmutableSet elementChildren = Children.Values.Select(child => child.ToExpression()).ToImmutableHashSet(); - return new IncludeElementExpression(_relationship, elementChildren); - } - } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs index cd95ef61a3..e76aaf0946 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs @@ -34,14 +34,24 @@ public override TResult Accept(QueryExpressionVisitor child.ToString()).OrderBy(name => name))); + builder.Append(string.Join(",", Children.Select(child => toFullString ? child.ToFullString() : child.ToString()).OrderBy(name => name))); builder.Append('}'); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs index 4597570ba3..a63d87719d 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs @@ -33,9 +33,19 @@ public override TResult Accept(QueryExpressionVisitor chains = IncludeChainConverter.GetRelationshipChains(this); - return string.Join(",", chains.Select(child => child.ToString()).OrderBy(name => name)); + return string.Join(",", chains.Select(field => toFullString ? field.ToFullString() : field.ToString()).OrderBy(name => name)); } public override bool Equals(object? obj) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IsTypeExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IsTypeExpression.cs new file mode 100644 index 0000000000..a30e31308b --- /dev/null +++ b/src/JsonApiDotNetCore/Queries/Expressions/IsTypeExpression.cs @@ -0,0 +1,88 @@ +using System.Text; +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries.Internal.Parsing; + +namespace JsonApiDotNetCore.Queries.Expressions; + +/// +/// Represents the "isType" filter function, resulting from text such as: isType(,men), isType(creator,men) or +/// isType(creator,men,equals(hasBeard,'true')) +/// +[PublicAPI] +public class IsTypeExpression : FilterExpression +{ + public ResourceFieldChainExpression? TargetToOneRelationship { get; } + public ResourceType DerivedType { get; } + public FilterExpression? Child { get; } + + public IsTypeExpression(ResourceFieldChainExpression? targetToOneRelationship, ResourceType derivedType, FilterExpression? child) + { + ArgumentGuard.NotNull(derivedType, nameof(derivedType)); + + TargetToOneRelationship = targetToOneRelationship; + DerivedType = derivedType; + Child = child; + } + + public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) + { + return visitor.VisitIsType(this, argument); + } + + public override string ToString() + { + return InnerToString(false); + } + + public override string ToFullString() + { + return InnerToString(true); + } + + private string InnerToString(bool toFullString) + { + var builder = new StringBuilder(); + builder.Append(Keywords.IsType); + builder.Append('('); + + if (TargetToOneRelationship != null) + { + builder.Append(toFullString ? TargetToOneRelationship.ToFullString() : TargetToOneRelationship); + } + + builder.Append(','); + builder.Append(DerivedType); + + if (Child != null) + { + builder.Append(','); + builder.Append(toFullString ? Child.ToFullString() : Child); + } + + builder.Append(')'); + return builder.ToString(); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (IsTypeExpression)obj; + + return Equals(TargetToOneRelationship, other.TargetToOneRelationship) && DerivedType.Equals(other.DerivedType) && Equals(Child, other.Child); + } + + public override int GetHashCode() + { + return HashCode.Combine(TargetToOneRelationship, DerivedType, Child); + } +} diff --git a/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs index bc5b4790ac..17c62f230f 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs @@ -28,6 +28,11 @@ public override string ToString() return $"'{value}'"; } + public override string ToFullString() + { + return ToString(); + } + public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs index 0308c04de2..c8d8ffb24b 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs @@ -47,12 +47,22 @@ public override TResult Accept(QueryExpressionVisitor term.ToString()))); + builder.Append(string.Join(",", Terms.Select(term => toFullString ? term.ToFullString() : term.ToString()))); builder.Append(')'); return builder.ToString(); diff --git a/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs index f528790fd3..a9c598402b 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs @@ -30,12 +30,26 @@ public override TResult Accept(QueryExpressionVisitor(QueryExpressionVisitor constant.ToString())); + return string.Join(",", Elements.Select(element => element.ToString())); + } + + public override string ToFullString() + { + return string.Join(",", Elements.Select(element => element.ToFullString())); } public override bool Equals(object? obj) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpression.cs index e442e6968d..2ff93dafe4 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpression.cs @@ -9,4 +9,6 @@ namespace JsonApiDotNetCore.Queries.Expressions; public abstract class QueryExpression { public abstract TResult Accept(QueryExpressionVisitor visitor, TArgument argument); + + public abstract string ToFullString(); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs index cd5937cd80..7051e81f73 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs @@ -91,6 +91,18 @@ public override QueryExpression VisitNullConstant(NullConstantExpression express return null; } + public override QueryExpression VisitIsType(IsTypeExpression expression, TArgument argument) + { + ResourceFieldChainExpression? newTargetToOneRelationship = expression.TargetToOneRelationship != null + ? Visit(expression.TargetToOneRelationship, argument) as ResourceFieldChainExpression + : null; + + FilterExpression? newChild = expression.Child != null ? Visit(expression.Child, argument) as FilterExpression : null; + + var newExpression = new IsTypeExpression(newTargetToOneRelationship, expression.DerivedType, newChild); + return newExpression.Equals(expression) ? expression : newExpression; + } + public override QueryExpression? VisitSortElement(SortElementExpression expression, TArgument argument) { SortElementExpression? newExpression = null; diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs index 7c893ba81c..7dcf44b1f4 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs @@ -53,6 +53,11 @@ public virtual TResult VisitHas(HasExpression expression, TArgument argument) return DefaultVisit(expression, argument); } + public virtual TResult VisitIsType(IsTypeExpression expression, TArgument argument) + { + return DefaultVisit(expression, argument); + } + public virtual TResult VisitSortElement(SortElementExpression expression, TArgument argument) { return DefaultVisit(expression, argument); diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs index db0e887c09..e567da8778 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs @@ -29,6 +29,11 @@ public override string ToString() return Scope == null ? ParameterName.ToString() : $"{ParameterName}: {Scope}"; } + public override string ToFullString() + { + return Scope == null ? ParameterName.ToFullString() : $"{ParameterName.ToFullString()}: {Scope.ToFullString()}"; + } + public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs index bffd8ae0ce..4cd035ba2d 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs @@ -21,8 +21,10 @@ public QueryableHandlerExpression(object queryableHandler, StringValues paramete _parameterValue = parameterValue; } +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection public IQueryable Apply(IQueryable query) where TResource : class, IIdentifiable +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection { var handler = (Func, StringValues, IQueryable>)_queryableHandler; return handler(query, _parameterValue); @@ -38,6 +40,11 @@ public override string ToString() return $"handler('{_parameterValue}')"; } + public override string ToFullString() + { + return ToString(); + } + public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs index 7f19c55ba0..7decec6221 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs @@ -36,6 +36,11 @@ public override string ToString() return string.Join(".", Fields.Select(field => field.PublicName)); } + public override string ToFullString() + { + return string.Join(".", Fields.Select(field => $"{field.Type.PublicName}:{field.PublicName}")); + } + public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs index 9de73655ad..78de440a42 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs @@ -35,6 +35,16 @@ public override TResult Accept(QueryExpressionVisitor child.ToString())); } + public override string ToFullString() + { + return string.Join(",", Elements.Select(child => child.ToFullString())); + } + public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs index c0532070e5..bc1e611bd8 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs @@ -26,7 +26,12 @@ public override TResult Accept(QueryExpressionVisitor child.PublicName).OrderBy(name => name)); + return string.Join(",", Fields.Select(field => field.PublicName).OrderBy(name => name)); + } + + public override string ToFullString() + { + return string.Join(".", Fields.Select(field => $"{field.Type.PublicName}:{field.PublicName}").OrderBy(name => name)); } public override bool Equals(object? obj) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs index f0e434cccd..53f9ff0eb6 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Queries.Expressions; public static class SparseFieldSetExpressionExtensions { public static SparseFieldSetExpression? Including(this SparseFieldSetExpression? sparseFieldSet, - Expression> fieldSelector, IResourceGraph resourceGraph) + Expression> fieldSelector, IResourceGraph resourceGraph) where TResource : class, IIdentifiable { ArgumentGuard.NotNull(fieldSelector, nameof(fieldSelector)); @@ -39,7 +39,7 @@ public static class SparseFieldSetExpressionExtensions } public static SparseFieldSetExpression? Excluding(this SparseFieldSetExpression? sparseFieldSet, - Expression> fieldSelector, IResourceGraph resourceGraph) + Expression> fieldSelector, IResourceGraph resourceGraph) where TResource : class, IIdentifiable { ArgumentGuard.NotNull(fieldSelector, nameof(fieldSelector)); diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs index 8ec77f12fc..8e52df9b3b 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs @@ -26,10 +26,20 @@ public override TResult Accept(QueryExpressionVisitor 0) { @@ -38,7 +48,7 @@ public override string ToString() builder.Append(resourceType.PublicName); builder.Append('('); - builder.Append(fields); + builder.Append(toFullString ? fieldSet.ToFullString() : fieldSet); builder.Append(')'); } diff --git a/src/JsonApiDotNetCore/Queries/FieldSelection.cs b/src/JsonApiDotNetCore/Queries/FieldSelection.cs new file mode 100644 index 0000000000..54c59005bf --- /dev/null +++ b/src/JsonApiDotNetCore/Queries/FieldSelection.cs @@ -0,0 +1,75 @@ +using System.Text; +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Queries; + +/// +/// Provides access to sparse fieldsets, per resource type. There's usually just a single resource type, but there can be multiple in case an endpoint +/// for an abstract resource type returns derived types. +/// +[PublicAPI] +public sealed class FieldSelection : Dictionary +{ + public bool IsEmpty => Values.All(selectors => selectors.IsEmpty); + + public IReadOnlySet GetResourceTypes() + { + return Keys.ToHashSet(); + } + +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection + public FieldSelectors GetOrCreateSelectors(ResourceType resourceType) +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection + { + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + + if (!ContainsKey(resourceType)) + { + this[resourceType] = new FieldSelectors(); + } + + return this[resourceType]; + } + + public override string ToString() + { + var builder = new StringBuilder(); + + var writer = new IndentingStringWriter(builder); + WriteSelection(writer); + + return builder.ToString(); + } + + internal void WriteSelection(IndentingStringWriter writer) + { + using (writer.Indent()) + { + foreach (ResourceType type in GetResourceTypes()) + { + writer.WriteLine($"{nameof(FieldSelectors)}<{type.ClrType.Name}>"); + WriterSelectors(writer, type); + } + } + } + + private void WriterSelectors(IndentingStringWriter writer, ResourceType type) + { + using (writer.Indent()) + { + foreach ((ResourceFieldAttribute field, QueryLayer? nextLayer) in GetOrCreateSelectors(type)) + { + if (nextLayer == null) + { + writer.WriteLine(field.ToString()); + } + else + { + nextLayer.WriteLayer(writer, $"{field.PublicName}: "); + } + } + } + } +} diff --git a/src/JsonApiDotNetCore/Queries/FieldSelectors.cs b/src/JsonApiDotNetCore/Queries/FieldSelectors.cs new file mode 100644 index 0000000000..a07b4f0c79 --- /dev/null +++ b/src/JsonApiDotNetCore/Queries/FieldSelectors.cs @@ -0,0 +1,70 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Queries; + +/// +/// A data structure that contains which fields (attributes and relationships) to retrieve, or empty to retrieve all. In the case of a relationship, it +/// contains the nested query constraints. +/// +[PublicAPI] +public sealed class FieldSelectors : Dictionary +{ + public bool IsEmpty => !this.Any(); + + public bool ContainsReadOnlyAttribute + { + get + { + return this.Any(selector => selector.Key is AttrAttribute attribute && attribute.Property.SetMethod == null); + } + } + + public bool ContainsOnlyRelationships + { + get + { + return this.All(selector => selector.Key is RelationshipAttribute); + } + } + + public bool ContainsField(ResourceFieldAttribute field) + { + ArgumentGuard.NotNull(field, nameof(field)); + + return ContainsKey(field); + } + + public void IncludeAttribute(AttrAttribute attribute) + { + ArgumentGuard.NotNull(attribute, nameof(attribute)); + + this[attribute] = null; + } + + public void IncludeAttributes(IEnumerable attributes) + { + ArgumentGuard.NotNull(attributes, nameof(attributes)); + + foreach (AttrAttribute attribute in attributes) + { + this[attribute] = null; + } + } + + public void IncludeRelationship(RelationshipAttribute relationship, QueryLayer? queryLayer) + { + ArgumentGuard.NotNull(relationship, nameof(relationship)); + + this[relationship] = queryLayer; + } + + public void RemoveAttributes() + { + while (this.Any(pair => pair.Key is AttrAttribute)) + { + ResourceFieldAttribute field = this.First(pair => pair.Key is AttrAttribute).Key; + Remove(field); + } + } +} diff --git a/src/JsonApiDotNetCore/Queries/IndentingStringWriter.cs b/src/JsonApiDotNetCore/Queries/IndentingStringWriter.cs new file mode 100644 index 0000000000..2d5a366c28 --- /dev/null +++ b/src/JsonApiDotNetCore/Queries/IndentingStringWriter.cs @@ -0,0 +1,41 @@ +using System.Text; + +namespace JsonApiDotNetCore.Queries; + +internal sealed class IndentingStringWriter : IDisposable +{ + private readonly StringBuilder _builder; + + private int _indentDepth; + + public IndentingStringWriter(StringBuilder builder) + { + _builder = builder; + } + + public void WriteLine(string? line) + { + if (_indentDepth > 0) + { + _builder.Append(new string(' ', _indentDepth * 2)); + } + + _builder.AppendLine(line); + } + + public IndentingStringWriter Indent() + { + WriteLine("{"); + _indentDepth++; + return this; + } + + public void Dispose() + { + if (_indentDepth > 0) + { + _indentDepth--; + WriteLine("}"); + } + } +} diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FieldChainInheritanceRequirement.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FieldChainInheritanceRequirement.cs new file mode 100644 index 0000000000..4b779d1ccd --- /dev/null +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FieldChainInheritanceRequirement.cs @@ -0,0 +1,17 @@ +namespace JsonApiDotNetCore.Queries.Internal.Parsing; + +/// +/// Indicates how to handle derived types when resolving resource field chains. +/// +internal enum FieldChainInheritanceRequirement +{ + /// + /// Do not consider derived types when resolving attributes or relationships. + /// + Disabled, + + /// + /// Consider derived types when resolving attributes or relationships, but fail when multiple matches are found. + /// + RequireSingleMatch +} diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs index c2b66f9063..705f057bc5 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs @@ -28,15 +28,16 @@ public FilterExpression Parse(string source, ResourceType resourceTypeInScope) { ArgumentGuard.NotNull(resourceTypeInScope, nameof(resourceTypeInScope)); - _resourceTypeInScope = resourceTypeInScope; - - Tokenize(source); + return InScopeOfResourceType(resourceTypeInScope, () => + { + Tokenize(source); - FilterExpression expression = ParseFilter(); + FilterExpression expression = ParseFilter(); - AssertTokenStackIsEmpty(); + AssertTokenStackIsEmpty(); - return expression; + return expression; + }); } protected FilterExpression ParseFilter() @@ -76,6 +77,10 @@ protected FilterExpression ParseFilter() { return ParseHas(); } + case Keywords.IsType: + { + return ParseIsType(); + } } } @@ -259,13 +264,92 @@ protected HasExpression ParseHas() private FilterExpression ParseFilterInHas(HasManyAttribute hasManyRelationship) { - ResourceType outerScopeBackup = _resourceTypeInScope!; + return InScopeOfResourceType(hasManyRelationship.RightType, ParseFilter); + } + + private IsTypeExpression ParseIsType() + { + EatText(Keywords.IsType); + EatSingleCharacterToken(TokenKind.OpenParen); + + ResourceFieldChainExpression? targetToOneRelationship = TryParseToOneRelationshipChain(); + + EatSingleCharacterToken(TokenKind.Comma); + + ResourceType baseType = targetToOneRelationship != null ? ((RelationshipAttribute)targetToOneRelationship.Fields[^1]).RightType : _resourceTypeInScope!; + ResourceType derivedType = ParseDerivedType(baseType); + + FilterExpression? child = TryParseFilterInIsType(derivedType); + + EatSingleCharacterToken(TokenKind.CloseParen); + + return new IsTypeExpression(targetToOneRelationship, derivedType, child); + } + + private ResourceFieldChainExpression? TryParseToOneRelationshipChain() + { + if (TokenStack.TryPeek(out Token? nextToken) && nextToken.Kind == TokenKind.Comma) + { + return null; + } + + return ParseFieldChain(FieldChainRequirements.EndsInToOne, "Relationship name or , expected."); + } + + private ResourceType ParseDerivedType(ResourceType baseType) + { + if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text) + { + string derivedTypeName = token.Value!; + return ResolveDerivedType(baseType, derivedTypeName); + } + + throw new QueryParseException("Resource type expected."); + } + + private ResourceType ResolveDerivedType(ResourceType baseType, string derivedTypeName) + { + ResourceType? derivedType = GetDerivedType(baseType, derivedTypeName); + + if (derivedType == null) + { + throw new QueryParseException($"Resource type '{derivedTypeName}' does not exist or does not derive from '{baseType.PublicName}'."); + } + + return derivedType; + } + + private ResourceType? GetDerivedType(ResourceType baseType, string publicName) + { + foreach (ResourceType derivedType in baseType.DirectlyDerivedTypes) + { + if (derivedType.PublicName == publicName) + { + return derivedType; + } + + ResourceType? nextType = GetDerivedType(derivedType, publicName); + + if (nextType != null) + { + return nextType; + } + } + + return null; + } + + private FilterExpression? TryParseFilterInIsType(ResourceType derivedType) + { + FilterExpression? filter = null; - _resourceTypeInScope = hasManyRelationship.RightType; + if (TokenStack.TryPeek(out Token? nextToken) && nextToken.Kind == TokenKind.Comma) + { + EatSingleCharacterToken(TokenKind.Comma); - FilterExpression filter = ParseFilter(); + filter = InScopeOfResourceType(derivedType, ParseFilter); + } - _resourceTypeInScope = outerScopeBackup; return filter; } @@ -341,12 +425,19 @@ protected override IImmutableList OnResolveFieldChain(st { if (chainRequirements == FieldChainRequirements.EndsInToMany) { - return ChainResolver.ResolveToOneChainEndingInToMany(_resourceTypeInScope!, path, _validateSingleFieldCallback); + return ChainResolver.ResolveToOneChainEndingInToMany(_resourceTypeInScope!, path, FieldChainInheritanceRequirement.Disabled, + _validateSingleFieldCallback); } if (chainRequirements == FieldChainRequirements.EndsInAttribute) { - return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceTypeInScope!, path, _validateSingleFieldCallback); + return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceTypeInScope!, path, FieldChainInheritanceRequirement.Disabled, + _validateSingleFieldCallback); + } + + if (chainRequirements == FieldChainRequirements.EndsInToOne) + { + return ChainResolver.ResolveToOneChain(_resourceTypeInScope!, path, _validateSingleFieldCallback); } if (chainRequirements.HasFlag(FieldChainRequirements.EndsInAttribute) && chainRequirements.HasFlag(FieldChainRequirements.EndsInToOne)) @@ -356,4 +447,19 @@ protected override IImmutableList OnResolveFieldChain(st throw new InvalidOperationException($"Unexpected combination of chain requirement flags '{chainRequirements}'."); } + + private TResult InScopeOfResourceType(ResourceType resourceType, Func action) + { + ResourceType? backupType = _resourceTypeInScope; + + try + { + _resourceTypeInScope = resourceType; + return action(); + } + finally + { + _resourceTypeInScope = backupType; + } + } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs index 95e51dca92..a453921989 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs @@ -1,6 +1,8 @@ using System.Collections.Immutable; +using System.Text; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; @@ -9,67 +11,266 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing; [PublicAPI] public class IncludeParser : QueryExpressionParser { - private static readonly IncludeChainConverter IncludeChainConverter = new(); - - private readonly Action? _validateSingleRelationshipCallback; - private ResourceType? _resourceTypeInScope; - - public IncludeParser(Action? validateSingleRelationshipCallback = null) - { - _validateSingleRelationshipCallback = validateSingleRelationshipCallback; - } + private static readonly ResourceFieldChainErrorFormatter ErrorFormatter = new(); public IncludeExpression Parse(string source, ResourceType resourceTypeInScope, int? maximumDepth) { ArgumentGuard.NotNull(resourceTypeInScope, nameof(resourceTypeInScope)); - _resourceTypeInScope = resourceTypeInScope; - Tokenize(source); - IncludeExpression expression = ParseInclude(maximumDepth); + IncludeExpression expression = ParseInclude(resourceTypeInScope, maximumDepth); AssertTokenStackIsEmpty(); + ValidateMaximumIncludeDepth(maximumDepth, expression); return expression; } - protected IncludeExpression ParseInclude(int? maximumDepth) + protected IncludeExpression ParseInclude(ResourceType resourceTypeInScope, int? maximumDepth) { - ResourceFieldChainExpression firstChain = ParseFieldChain(FieldChainRequirements.IsRelationship, "Relationship name expected."); + var treeRoot = IncludeTreeNode.CreateRoot(resourceTypeInScope); - List chains = firstChain.AsList(); + ParseRelationshipChain(treeRoot); while (TokenStack.Any()) { EatSingleCharacterToken(TokenKind.Comma); - ResourceFieldChainExpression nextChain = ParseFieldChain(FieldChainRequirements.IsRelationship, "Relationship name expected."); - chains.Add(nextChain); + ParseRelationshipChain(treeRoot); } - ValidateMaximumIncludeDepth(maximumDepth, chains); + return treeRoot.ToExpression(); + } + + private void ParseRelationshipChain(IncludeTreeNode treeRoot) + { + // A relationship name usually matches a single relationship, even when overridden in derived types. + // But in the following case, two relationships are matched on GET /shoppingBaskets?include=items: + // + // public abstract class ShoppingBasket : Identifiable + // { + // } + // + // public sealed class SilverShoppingBasket : ShoppingBasket + // { + // [HasMany] + // public ISet
Items { get; get; } + // } + // + // public sealed class PlatinumShoppingBasket : ShoppingBasket + // { + // [HasMany] + // public ISet Items { get; get; } + // } + // + // Now if the include chain has subsequent relationships, we need to scan both Items relationships for matches, + // which is why ParseRelationshipName returns a collection. + // + // The advantage of this unfolding is we don't require callers to upcast in relationship chains. The downside is + // that there's currently no way to include Products without Articles. We could add such optional upcast syntax + // in the future, if desired. + + ICollection children = ParseRelationshipName(treeRoot.AsList()); + + while (TokenStack.TryPeek(out Token? nextToken) && nextToken.Kind == TokenKind.Period) + { + EatSingleCharacterToken(TokenKind.Period); - return IncludeChainConverter.FromRelationshipChains(chains); + children = ParseRelationshipName(children); + } } - private static void ValidateMaximumIncludeDepth(int? maximumDepth, IEnumerable chains) + private ICollection ParseRelationshipName(ICollection parents) + { + if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text) + { + return LookupRelationshipName(token.Value!, parents); + } + + throw new QueryParseException("Relationship name expected."); + } + + private ICollection LookupRelationshipName(string relationshipName, ICollection parents) + { + List children = new(); + HashSet relationshipsFound = new(); + + foreach (IncludeTreeNode parent in parents) + { + // Depending on the left side of the include chain, we may match relationships anywhere in the resource type hierarchy. + // This is compensated for when rendering the response, which substitutes relationships on base types with the derived ones. + IReadOnlySet relationships = parent.Relationship.RightType.GetRelationshipsInTypeOrDerived(relationshipName); + + if (relationships.Any()) + { + relationshipsFound.AddRange(relationships); + + RelationshipAttribute[] relationshipsToInclude = relationships.Where(relationship => relationship.CanInclude).ToArray(); + ICollection affectedChildren = parent.EnsureChildren(relationshipsToInclude); + children.AddRange(affectedChildren); + } + } + + AssertRelationshipsFound(relationshipsFound, relationshipName, parents); + AssertAtLeastOneCanBeIncluded(relationshipsFound, relationshipName, parents); + + return children; + } + + private static void AssertRelationshipsFound(ISet relationshipsFound, string relationshipName, ICollection parents) + { + if (relationshipsFound.Any()) + { + return; + } + + string[] parentPaths = parents.Select(parent => parent.Path).Distinct().Where(path => path != string.Empty).ToArray(); + string path = parentPaths.Length > 0 ? $"{parentPaths[0]}.{relationshipName}" : relationshipName; + + ResourceType[] parentResourceTypes = parents.Select(parent => parent.Relationship.RightType).Distinct().ToArray(); + + bool hasDerivedTypes = parents.Any(parent => parent.Relationship.RightType.DirectlyDerivedTypes.Count > 0); + + string message = ErrorFormatter.GetForNoneFound(ResourceFieldCategory.Relationship, relationshipName, path, parentResourceTypes, hasDerivedTypes); + throw new QueryParseException(message); + } + + private static void AssertAtLeastOneCanBeIncluded(ISet relationshipsFound, string relationshipName, + ICollection parents) + { + if (relationshipsFound.All(relationship => !relationship.CanInclude)) + { + string parentPath = parents.First().Path; + ResourceType resourceType = relationshipsFound.First().LeftType; + + string message = parentPath == string.Empty + ? $"Including the relationship '{relationshipName}' on '{resourceType}' is not allowed." + : $"Including the relationship '{relationshipName}' in '{parentPath}.{relationshipName}' on '{resourceType}' is not allowed."; + + throw new InvalidQueryStringParameterException("include", "Including the requested relationship is not allowed.", message); + } + } + + private static void ValidateMaximumIncludeDepth(int? maximumDepth, IncludeExpression include) { if (maximumDepth != null) { - foreach (ResourceFieldChainExpression chain in chains) + Stack parentChain = new(); + + foreach (IncludeElementExpression element in include.Elements) { - if (chain.Fields.Count > maximumDepth) - { - string path = string.Join('.', chain.Fields.Select(field => field.PublicName)); - throw new QueryParseException($"Including '{path}' exceeds the maximum inclusion depth of {maximumDepth}."); - } + ThrowIfMaximumDepthExceeded(element, parentChain, maximumDepth.Value); } } } + private static void ThrowIfMaximumDepthExceeded(IncludeElementExpression includeElement, Stack parentChain, int maximumDepth) + { + parentChain.Push(includeElement.Relationship); + + if (parentChain.Count > maximumDepth) + { + string path = string.Join('.', parentChain.Reverse().Select(relationship => relationship.PublicName)); + throw new QueryParseException($"Including '{path}' exceeds the maximum inclusion depth of {maximumDepth}."); + } + + foreach (IncludeElementExpression child in includeElement.Children) + { + ThrowIfMaximumDepthExceeded(child, parentChain, maximumDepth); + } + + parentChain.Pop(); + } + protected override IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { - return ChainResolver.ResolveRelationshipChain(_resourceTypeInScope!, path, _validateSingleRelationshipCallback); + throw new NotSupportedException(); + } + + private sealed class IncludeTreeNode + { + private readonly IncludeTreeNode? _parent; + private readonly IDictionary _children = new Dictionary(); + + public RelationshipAttribute Relationship { get; } + + public string Path + { + get + { + var pathBuilder = new StringBuilder(); + IncludeTreeNode? parent = this; + + while (parent is { Relationship: not HiddenRootRelationship }) + { + pathBuilder.Insert(0, pathBuilder.Length > 0 ? $"{parent.Relationship.PublicName}." : parent.Relationship.PublicName); + parent = parent._parent; + } + + return pathBuilder.ToString(); + } + } + + private IncludeTreeNode(RelationshipAttribute relationship, IncludeTreeNode? parent) + { + Relationship = relationship; + _parent = parent; + } + + public static IncludeTreeNode CreateRoot(ResourceType resourceType) + { + var relationship = new HiddenRootRelationship(resourceType); + return new IncludeTreeNode(relationship, null); + } + + public ICollection EnsureChildren(ICollection relationships) + { + foreach (RelationshipAttribute relationship in relationships) + { + if (!_children.ContainsKey(relationship)) + { + var newChild = new IncludeTreeNode(relationship, this); + _children.Add(relationship, newChild); + } + } + + return _children.Where(pair => relationships.Contains(pair.Key)).Select(pair => pair.Value).ToList(); + } + + public IncludeExpression ToExpression() + { + IncludeElementExpression element = ToElementExpression(); + + if (element.Relationship is HiddenRootRelationship) + { + return new IncludeExpression(element.Children); + } + + return new IncludeExpression(ImmutableHashSet.Create(element)); + } + + private IncludeElementExpression ToElementExpression() + { + IImmutableSet elementChildren = _children.Values.Select(child => child.ToElementExpression()).ToImmutableHashSet(); + return new IncludeElementExpression(Relationship, elementChildren); + } + + public override string ToString() + { + IncludeExpression include = ToExpression(); + return include.ToFullString(); + } + + private sealed class HiddenRootRelationship : RelationshipAttribute + { + public HiddenRootRelationship(ResourceType rightType) + { + ArgumentGuard.NotNull(rightType, nameof(rightType)); + + RightType = rightType; + PublicName = "<>"; + } + } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/Keywords.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/Keywords.cs index dd0bda51b7..790f8f544d 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/Keywords.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/Keywords.cs @@ -23,4 +23,5 @@ public static class Keywords public const string Any = "any"; public const string Count = "count"; public const string Has = "has"; + public const string IsType = "isType"; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs index 4dc7230c24..681c1dd8f4 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Text; using JetBrains.Annotations; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; @@ -31,17 +32,42 @@ protected virtual void Tokenize(string source) protected ResourceFieldChainExpression ParseFieldChain(FieldChainRequirements chainRequirements, string? alternativeErrorMessage) { - if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text) + var pathBuilder = new StringBuilder(); + EatFieldChain(pathBuilder, alternativeErrorMessage); + + IImmutableList chain = OnResolveFieldChain(pathBuilder.ToString(), chainRequirements); + + if (chain.Any()) { - IImmutableList chain = OnResolveFieldChain(token.Value!, chainRequirements); + return new ResourceFieldChainExpression(chain); + } + + throw new QueryParseException(alternativeErrorMessage ?? "Field name expected."); + } - if (chain.Any()) + private void EatFieldChain(StringBuilder pathBuilder, string? alternativeErrorMessage) + { + while (true) + { + if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text) + { + pathBuilder.Append(token.Value); + + if (TokenStack.TryPeek(out Token? nextToken) && nextToken.Kind == TokenKind.Period) + { + EatSingleCharacterToken(TokenKind.Period); + pathBuilder.Append('.'); + } + else + { + return; + } + } + else { - return new ResourceFieldChainExpression(chain); + throw new QueryParseException(alternativeErrorMessage ?? "Field name expected."); } } - - throw new QueryParseException(alternativeErrorMessage ?? "Field name expected."); } protected CountExpression? TryParseCount() diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs index 6676cae30f..3f04ce92aa 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs @@ -14,6 +14,7 @@ public sealed class QueryTokenizer [')'] = TokenKind.CloseParen, ['['] = TokenKind.OpenBracket, [']'] = TokenKind.CloseBracket, + ['.'] = TokenKind.Period, [','] = TokenKind.Comma, [':'] = TokenKind.Colon, ['-'] = TokenKind.Minus diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldCategory.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldCategory.cs new file mode 100644 index 0000000000..6630cf2767 --- /dev/null +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldCategory.cs @@ -0,0 +1,8 @@ +namespace JsonApiDotNetCore.Queries.Internal.Parsing; + +internal enum ResourceFieldCategory +{ + Field, + Attribute, + Relationship +} diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainErrorFormatter.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainErrorFormatter.cs new file mode 100644 index 0000000000..e15b14893a --- /dev/null +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainErrorFormatter.cs @@ -0,0 +1,83 @@ +using System.Text; +using JsonApiDotNetCore.Configuration; + +namespace JsonApiDotNetCore.Queries.Internal.Parsing; + +internal sealed class ResourceFieldChainErrorFormatter +{ + public string GetForNotFound(ResourceFieldCategory category, string publicName, string path, ResourceType resourceType, + FieldChainInheritanceRequirement inheritanceRequirement) + { + var builder = new StringBuilder(); + WriteSource(category, publicName, builder); + WritePath(path, publicName, builder); + + builder.Append($" does not exist on resource type '{resourceType.PublicName}'"); + + if (inheritanceRequirement != FieldChainInheritanceRequirement.Disabled && resourceType.DirectlyDerivedTypes.Any()) + { + builder.Append(" or any of its derived types"); + } + + builder.Append('.'); + + return builder.ToString(); + } + + public string GetForMultipleMatches(ResourceFieldCategory category, string publicName, string path) + { + var builder = new StringBuilder(); + WriteSource(category, publicName, builder); + WritePath(path, publicName, builder); + + builder.Append(" is defined on multiple derived types."); + + return builder.ToString(); + } + + public string GetForWrongFieldType(ResourceFieldCategory category, string publicName, string path, ResourceType resourceType, string expected) + { + var builder = new StringBuilder(); + WriteSource(category, publicName, builder); + WritePath(path, publicName, builder); + + builder.Append($" must be {expected} on resource type '{resourceType.PublicName}'."); + + return builder.ToString(); + } + + public string GetForNoneFound(ResourceFieldCategory category, string publicName, string path, ICollection parentResourceTypes, + bool hasDerivedTypes) + { + var builder = new StringBuilder(); + WriteSource(category, publicName, builder); + WritePath(path, publicName, builder); + + if (parentResourceTypes.Count == 1) + { + builder.Append($" does not exist on resource type '{parentResourceTypes.First().PublicName}'"); + } + else + { + string typeNames = string.Join(", ", parentResourceTypes.Select(type => $"'{type.PublicName}'")); + builder.Append($" does not exist on any of the resource types {typeNames}"); + } + + builder.Append(hasDerivedTypes ? " or any of its derived types." : "."); + + return builder.ToString(); + } + + private static void WriteSource(ResourceFieldCategory category, string publicName, StringBuilder builder) + { + builder.Append($"{category} '{publicName}'"); + } + + private static void WritePath(string path, string publicName, StringBuilder builder) + { + if (path != publicName) + { + builder.Append($" in '{path}'"); + } + } +} diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs index 33f3643aa8..4fb2632557 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs @@ -9,6 +9,34 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing; ///
internal sealed class ResourceFieldChainResolver { + private static readonly ResourceFieldChainErrorFormatter ErrorFormatter = new(); + + /// + /// Resolves a chain of to-one relationships. + /// author + /// + /// author.address.country + /// + /// + public IImmutableList ResolveToOneChain(ResourceType resourceType, string path, + Action? validateCallback = null) + { + ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); + ResourceType nextResourceType = resourceType; + + foreach (string publicName in path.Split(".")) + { + RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceType, path, FieldChainInheritanceRequirement.Disabled); + + validateCallback?.Invoke(toOneRelationship, nextResourceType, path); + + chainBuilder.Add(toOneRelationship); + nextResourceType = toOneRelationship.RightType; + } + + return chainBuilder.ToImmutable(); + } + /// /// Resolves a chain of relationships that ends in a to-many relationship, for example: blogs.owner.articles.comments /// @@ -22,7 +50,7 @@ public IImmutableList ResolveToManyChain(ResourceType re foreach (string publicName in publicNameParts[..^1]) { - RelationshipAttribute relationship = GetRelationship(publicName, nextResourceType, path); + RelationshipAttribute relationship = GetRelationship(publicName, nextResourceType, path, FieldChainInheritanceRequirement.Disabled); validateCallback?.Invoke(relationship, nextResourceType, path); @@ -31,7 +59,7 @@ public IImmutableList ResolveToManyChain(ResourceType re } string lastName = publicNameParts[^1]; - RelationshipAttribute lastToManyRelationship = GetToManyRelationship(lastName, nextResourceType, path); + RelationshipAttribute lastToManyRelationship = GetToManyRelationship(lastName, nextResourceType, path, FieldChainInheritanceRequirement.Disabled); validateCallback?.Invoke(lastToManyRelationship, nextResourceType, path); @@ -59,7 +87,7 @@ public IImmutableList ResolveRelationshipChain(ResourceT foreach (string publicName in path.Split(".")) { - RelationshipAttribute relationship = GetRelationship(publicName, nextResourceType, path); + RelationshipAttribute relationship = GetRelationship(publicName, nextResourceType, path, FieldChainInheritanceRequirement.Disabled); validateCallback?.Invoke(relationship, nextResourceType, path); @@ -78,7 +106,7 @@ public IImmutableList ResolveRelationshipChain(ResourceT /// name ///
public IImmutableList ResolveToOneChainEndingInAttribute(ResourceType resourceType, string path, - Action? validateCallback = null) + FieldChainInheritanceRequirement inheritanceRequirement, Action? validateCallback = null) { ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); @@ -87,7 +115,7 @@ public IImmutableList ResolveToOneChainEndingInAttribute foreach (string publicName in publicNameParts[..^1]) { - RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceType, path); + RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceType, path, inheritanceRequirement); validateCallback?.Invoke(toOneRelationship, nextResourceType, path); @@ -96,7 +124,7 @@ public IImmutableList ResolveToOneChainEndingInAttribute } string lastName = publicNameParts[^1]; - AttrAttribute lastAttribute = GetAttribute(lastName, nextResourceType, path); + AttrAttribute lastAttribute = GetAttribute(lastName, nextResourceType, path, inheritanceRequirement); validateCallback?.Invoke(lastAttribute, nextResourceType, path); @@ -114,7 +142,7 @@ public IImmutableList ResolveToOneChainEndingInAttribute /// ///
public IImmutableList ResolveToOneChainEndingInToMany(ResourceType resourceType, string path, - Action? validateCallback = null) + FieldChainInheritanceRequirement inheritanceRequirement, Action? validateCallback = null) { ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); @@ -123,7 +151,7 @@ public IImmutableList ResolveToOneChainEndingInToMany(Re foreach (string publicName in publicNameParts[..^1]) { - RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceType, path); + RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceType, path, inheritanceRequirement); validateCallback?.Invoke(toOneRelationship, nextResourceType, path); @@ -133,7 +161,7 @@ public IImmutableList ResolveToOneChainEndingInToMany(Re string lastName = publicNameParts[^1]; - RelationshipAttribute toManyRelationship = GetToManyRelationship(lastName, nextResourceType, path); + RelationshipAttribute toManyRelationship = GetToManyRelationship(lastName, nextResourceType, path, inheritanceRequirement); validateCallback?.Invoke(toManyRelationship, nextResourceType, path); @@ -160,7 +188,7 @@ public IImmutableList ResolveToOneChainEndingInAttribute foreach (string publicName in publicNameParts[..^1]) { - RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceType, path); + RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceType, path, FieldChainInheritanceRequirement.Disabled); validateCallback?.Invoke(toOneRelationship, nextResourceType, path); @@ -173,9 +201,10 @@ public IImmutableList ResolveToOneChainEndingInAttribute if (lastField is HasManyAttribute) { - throw new QueryParseException(path == lastName - ? $"Field '{lastName}' must be an attribute or a to-one relationship on resource type '{nextResourceType.PublicName}'." - : $"Field '{lastName}' in '{path}' must be an attribute or a to-one relationship on resource type '{nextResourceType.PublicName}'."); + string message = ErrorFormatter.GetForWrongFieldType(ResourceFieldCategory.Field, lastName, path, nextResourceType, + "an attribute or a to-one relationship"); + + throw new QueryParseException(message); } validateCallback?.Invoke(lastField, nextResourceType, path); @@ -184,60 +213,75 @@ public IImmutableList ResolveToOneChainEndingInAttribute return chainBuilder.ToImmutable(); } - private RelationshipAttribute GetRelationship(string publicName, ResourceType resourceType, string path) + private RelationshipAttribute GetRelationship(string publicName, ResourceType resourceType, string path, + FieldChainInheritanceRequirement inheritanceRequirement) { - RelationshipAttribute? relationship = resourceType.FindRelationshipByPublicName(publicName); + IReadOnlyCollection relationships = inheritanceRequirement == FieldChainInheritanceRequirement.Disabled + ? resourceType.FindRelationshipByPublicName(publicName)?.AsArray() ?? Array.Empty() + : resourceType.GetRelationshipsInTypeOrDerived(publicName); - if (relationship == null) + if (relationships.Count == 0) { - throw new QueryParseException(path == publicName - ? $"Relationship '{publicName}' does not exist on resource type '{resourceType.PublicName}'." - : $"Relationship '{publicName}' in '{path}' does not exist on resource type '{resourceType.PublicName}'."); + string message = ErrorFormatter.GetForNotFound(ResourceFieldCategory.Relationship, publicName, path, resourceType, inheritanceRequirement); + throw new QueryParseException(message); } - return relationship; + if (inheritanceRequirement == FieldChainInheritanceRequirement.RequireSingleMatch && relationships.Count > 1) + { + string message = ErrorFormatter.GetForMultipleMatches(ResourceFieldCategory.Relationship, publicName, path); + throw new QueryParseException(message); + } + + return relationships.First(); } - private RelationshipAttribute GetToManyRelationship(string publicName, ResourceType resourceType, string path) + private RelationshipAttribute GetToManyRelationship(string publicName, ResourceType resourceType, string path, + FieldChainInheritanceRequirement inheritanceRequirement) { - RelationshipAttribute relationship = GetRelationship(publicName, resourceType, path); + RelationshipAttribute relationship = GetRelationship(publicName, resourceType, path, inheritanceRequirement); if (relationship is not HasManyAttribute) { - throw new QueryParseException(path == publicName - ? $"Relationship '{publicName}' must be a to-many relationship on resource type '{resourceType.PublicName}'." - : $"Relationship '{publicName}' in '{path}' must be a to-many relationship on resource type '{resourceType.PublicName}'."); + string message = ErrorFormatter.GetForWrongFieldType(ResourceFieldCategory.Relationship, publicName, path, resourceType, "a to-many relationship"); + throw new QueryParseException(message); } return relationship; } - private RelationshipAttribute GetToOneRelationship(string publicName, ResourceType resourceType, string path) + private RelationshipAttribute GetToOneRelationship(string publicName, ResourceType resourceType, string path, + FieldChainInheritanceRequirement inheritanceRequirement) { - RelationshipAttribute relationship = GetRelationship(publicName, resourceType, path); + RelationshipAttribute relationship = GetRelationship(publicName, resourceType, path, inheritanceRequirement); if (relationship is not HasOneAttribute) { - throw new QueryParseException(path == publicName - ? $"Relationship '{publicName}' must be a to-one relationship on resource type '{resourceType.PublicName}'." - : $"Relationship '{publicName}' in '{path}' must be a to-one relationship on resource type '{resourceType.PublicName}'."); + string message = ErrorFormatter.GetForWrongFieldType(ResourceFieldCategory.Relationship, publicName, path, resourceType, "a to-one relationship"); + throw new QueryParseException(message); } return relationship; } - private AttrAttribute GetAttribute(string publicName, ResourceType resourceType, string path) + private AttrAttribute GetAttribute(string publicName, ResourceType resourceType, string path, FieldChainInheritanceRequirement inheritanceRequirement) { - AttrAttribute? attribute = resourceType.FindAttributeByPublicName(publicName); + IReadOnlyCollection attributes = inheritanceRequirement == FieldChainInheritanceRequirement.Disabled + ? resourceType.FindAttributeByPublicName(publicName)?.AsArray() ?? Array.Empty() + : resourceType.GetAttributesInTypeOrDerived(publicName); - if (attribute == null) + if (attributes.Count == 0) { - throw new QueryParseException(path == publicName - ? $"Attribute '{publicName}' does not exist on resource type '{resourceType.PublicName}'." - : $"Attribute '{publicName}' in '{path}' does not exist on resource type '{resourceType.PublicName}'."); + string message = ErrorFormatter.GetForNotFound(ResourceFieldCategory.Attribute, publicName, path, resourceType, inheritanceRequirement); + throw new QueryParseException(message); } - return attribute; + if (inheritanceRequirement == FieldChainInheritanceRequirement.RequireSingleMatch && attributes.Count > 1) + { + string message = ErrorFormatter.GetForMultipleMatches(ResourceFieldCategory.Attribute, publicName, path); + throw new QueryParseException(message); + } + + return attributes.First(); } public ResourceFieldAttribute GetField(string publicName, ResourceType resourceType, string path) @@ -246,9 +290,10 @@ public ResourceFieldAttribute GetField(string publicName, ResourceType resourceT if (field == null) { - throw new QueryParseException(path == publicName - ? $"Field '{publicName}' does not exist on resource type '{resourceType.PublicName}'." - : $"Field '{publicName}' in '{path}' does not exist on resource type '{resourceType.PublicName}'."); + string message = ErrorFormatter.GetForNotFound(ResourceFieldCategory.Field, publicName, path, resourceType, + FieldChainInheritanceRequirement.Disabled); + + throw new QueryParseException(message); } return field; diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs index 38a263e063..84782c2b3e 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs @@ -74,14 +74,40 @@ protected SortElementExpression ParseSortElement() protected override IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { + // An attribute or relationship name usually matches a single field, even when overridden in derived types. + // But in the following case, two attributes are matched on GET /shoppingBaskets?sort=bonusPoints: + // + // public abstract class ShoppingBasket : Identifiable + // { + // } + // + // public sealed class SilverShoppingBasket : ShoppingBasket + // { + // [Attr] + // public short BonusPoints { get; set; } + // } + // + // public sealed class PlatinumShoppingBasket : ShoppingBasket + // { + // [Attr] + // public long BonusPoints { get; set; } + // } + // + // In this case there are two distinct BonusPoints fields (with different data types). And the sort order depends + // on which attribute is used. + // + // Because there is no syntax to pick one, we fail with an error. We could add such optional upcast syntax + // (which would be required in this case) in the future to make it work, if desired. + if (chainRequirements == FieldChainRequirements.EndsInToMany) { - return ChainResolver.ResolveToOneChainEndingInToMany(_resourceTypeInScope!, path); + return ChainResolver.ResolveToOneChainEndingInToMany(_resourceTypeInScope!, path, FieldChainInheritanceRequirement.RequireSingleMatch); } if (chainRequirements == FieldChainRequirements.EndsInAttribute) { - return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceTypeInScope!, path, _validateSingleFieldCallback); + return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceTypeInScope!, path, FieldChainInheritanceRequirement.RequireSingleMatch, + _validateSingleFieldCallback); } throw new InvalidOperationException($"Unexpected combination of chain requirement flags '{chainRequirements}'."); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs index e7c96d21d5..b23dfdfea1 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs @@ -37,14 +37,14 @@ private ResourceType ParseSparseFieldTarget() EatSingleCharacterToken(TokenKind.OpenBracket); - ResourceType resourceType = ParseResourceName(); + ResourceType resourceType = ParseResourceType(); EatSingleCharacterToken(TokenKind.CloseBracket); return resourceType; } - private ResourceType ParseResourceName() + private ResourceType ParseResourceType() { if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/Token.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/Token.cs index 14b6289b83..bff295acae 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/Token.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/Token.cs @@ -8,9 +8,14 @@ public sealed class Token public TokenKind Kind { get; } public string? Value { get; } - public Token(TokenKind kind, string? value = null) + public Token(TokenKind kind) { Kind = kind; + } + + public Token(TokenKind kind, string value) + : this(kind) + { Value = value; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/TokenKind.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/TokenKind.cs index 75b6952f5a..f73cbd3418 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/TokenKind.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/TokenKind.cs @@ -6,6 +6,7 @@ public enum TokenKind CloseParen, OpenBracket, CloseBracket, + Period, Comma, Colon, Minus, diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index f7b95c3c86..40af882044 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -167,7 +167,7 @@ private QueryLayer ComposeTopLayer(IEnumerable constraints, R Filter = GetFilter(expressionsInTopScope, resourceType), Sort = GetSort(expressionsInTopScope, resourceType), Pagination = topPagination, - Projection = GetProjectionForSparseAttributeSet(resourceType) + Selection = GetSelectionForSparseAttributeSet(resourceType) }; } @@ -203,9 +203,10 @@ private IImmutableSet ProcessIncludeSet(IImmutableSet< foreach (IncludeElementExpression includeElement in includeElementsEvaluated) { - parentLayer.Projection ??= new Dictionary(); + parentLayer.Selection ??= new FieldSelection(); + FieldSelectors selectors = parentLayer.Selection.GetOrCreateSelectors(parentLayer.ResourceType); - if (!parentLayer.Projection.ContainsKey(includeElement.Relationship)) + if (!selectors.ContainsField(includeElement.Relationship)) { var relationshipChain = new List(parentRelationshipChain) { @@ -232,10 +233,10 @@ private IImmutableSet ProcessIncludeSet(IImmutableSet< Filter = isToManyRelationship ? GetFilter(expressionsInCurrentScope, resourceType) : null, Sort = isToManyRelationship ? GetSort(expressionsInCurrentScope, resourceType) : null, Pagination = isToManyRelationship ? GetPagination(expressionsInCurrentScope, resourceType) : null, - Projection = GetProjectionForSparseAttributeSet(resourceType) + Selection = GetSelectionForSparseAttributeSet(resourceType) }; - parentLayer.Projection.Add(includeElement.Relationship, child); + selectors.IncludeRelationship(includeElement.Relationship, child); IImmutableSet updatedChildren = ProcessIncludeSet(includeElement.Children, child, relationshipChain, constraints); @@ -278,18 +279,15 @@ public QueryLayer ComposeForGetById(TId id, ResourceType primaryResourceTyp if (fieldSelection == TopFieldSelection.OnlyIdAttribute) { - queryLayer.Projection = new Dictionary - { - [idAttribute] = null - }; + queryLayer.Selection = new FieldSelection(); + FieldSelectors selectors = queryLayer.Selection.GetOrCreateSelectors(primaryResourceType); + selectors.IncludeAttribute(idAttribute); } - else if (fieldSelection == TopFieldSelection.WithAllAttributes && queryLayer.Projection != null) + else if (fieldSelection == TopFieldSelection.WithAllAttributes && queryLayer.Selection != null) { // Discard any top-level ?fields[]= or attribute exclusions from resource definition, because we need the full database row. - while (queryLayer.Projection.Any(pair => pair.Key is AttrAttribute)) - { - queryLayer.Projection.Remove(queryLayer.Projection.First(pair => pair.Key is AttrAttribute)); - } + FieldSelectors selectors = queryLayer.Selection.GetOrCreateSelectors(primaryResourceType); + selectors.RemoveAttributes(); } return queryLayer; @@ -301,17 +299,21 @@ public QueryLayer ComposeSecondaryLayerForRelationship(ResourceType secondaryRes ArgumentGuard.NotNull(secondaryResourceType, nameof(secondaryResourceType)); QueryLayer secondaryLayer = ComposeFromConstraints(secondaryResourceType); - secondaryLayer.Projection = GetProjectionForRelationship(secondaryResourceType); + secondaryLayer.Selection = GetSelectionForRelationship(secondaryResourceType); secondaryLayer.Include = null; return secondaryLayer; } - private IDictionary GetProjectionForRelationship(ResourceType secondaryResourceType) + private FieldSelection GetSelectionForRelationship(ResourceType secondaryResourceType) { + var selection = new FieldSelection(); + FieldSelectors selectors = selection.GetOrCreateSelectors(secondaryResourceType); + IImmutableSet secondaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(secondaryResourceType); + selectors.IncludeAttributes(secondaryAttributeSet); - return secondaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer?)null); + return selection; } /// @@ -325,12 +327,12 @@ public QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, IncludeExpression? innerInclude = secondaryLayer.Include; secondaryLayer.Include = null; - IImmutableSet primaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(primaryResourceType); - - Dictionary primaryProjection = - primaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer?)null); + var primarySelection = new FieldSelection(); + FieldSelectors primarySelectors = primarySelection.GetOrCreateSelectors(primaryResourceType); - primaryProjection[relationship] = secondaryLayer; + IImmutableSet primaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(primaryResourceType); + primarySelectors.IncludeAttributes(primaryAttributeSet); + primarySelectors.IncludeRelationship(relationship, secondaryLayer); FilterExpression? primaryFilter = GetFilter(Array.Empty(), primaryResourceType); AttrAttribute primaryIdAttribute = GetIdAttribute(primaryResourceType); @@ -339,7 +341,7 @@ public QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, { Include = RewriteIncludeForSecondaryEndpoint(innerInclude, relationship), Filter = CreateFilterByIds(primaryId.AsArray(), primaryIdAttribute, primaryFilter), - Projection = primaryProjection + Selection = primarySelection }; } @@ -387,7 +389,7 @@ public QueryLayer ComposeForUpdate(TId id, ResourceType primaryResourceType primaryLayer.Sort = null; primaryLayer.Pagination = null; primaryLayer.Filter = CreateFilterByIds(id.AsArray(), primaryIdAttribute, primaryLayer.Filter); - primaryLayer.Projection = null; + primaryLayer.Selection = null; return primaryLayer; } @@ -400,7 +402,7 @@ public QueryLayer ComposeForUpdate(TId id, ResourceType primaryResourceType foreach (RelationshipAttribute relationship in _targetedFields.Relationships) { object? rightValue = relationship.GetValue(primaryResource); - ICollection rightResourceIds = _collectionConverter.ExtractResources(rightValue); + HashSet rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); if (rightResourceIds.Any()) { @@ -418,19 +420,20 @@ public QueryLayer ComposeForGetRelationshipRightIds(RelationshipAttribute relati AttrAttribute rightIdAttribute = GetIdAttribute(relationship.RightType); - object[] typedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); + HashSet typedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToHashSet(); FilterExpression? baseFilter = GetFilter(Array.Empty(), relationship.RightType); FilterExpression? filter = CreateFilterByIds(typedIds, rightIdAttribute, baseFilter); + var selection = new FieldSelection(); + FieldSelectors selectors = selection.GetOrCreateSelectors(relationship.RightType); + selectors.IncludeAttribute(rightIdAttribute); + return new QueryLayer(relationship.RightType) { Include = IncludeExpression.Empty, Filter = filter, - Projection = new Dictionary - { - [rightIdAttribute] = null - } + Selection = selection }; } @@ -442,27 +445,31 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T AttrAttribute leftIdAttribute = GetIdAttribute(hasManyRelationship.LeftType); AttrAttribute rightIdAttribute = GetIdAttribute(hasManyRelationship.RightType); - object[] rightTypedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); + HashSet rightTypedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToHashSet(); FilterExpression? leftFilter = CreateFilterByIds(leftId.AsArray(), leftIdAttribute, null); FilterExpression? rightFilter = CreateFilterByIds(rightTypedIds, rightIdAttribute, null); + var secondarySelection = new FieldSelection(); + FieldSelectors secondarySelectors = secondarySelection.GetOrCreateSelectors(hasManyRelationship.RightType); + secondarySelectors.IncludeAttribute(rightIdAttribute); + + QueryLayer secondaryLayer = new(hasManyRelationship.RightType) + { + Filter = rightFilter, + Selection = secondarySelection + }; + + var primarySelection = new FieldSelection(); + FieldSelectors primarySelectors = primarySelection.GetOrCreateSelectors(hasManyRelationship.LeftType); + primarySelectors.IncludeRelationship(hasManyRelationship, secondaryLayer); + primarySelectors.IncludeAttribute(leftIdAttribute); + return new QueryLayer(hasManyRelationship.LeftType) { Include = new IncludeExpression(ImmutableHashSet.Create(new IncludeElementExpression(hasManyRelationship))), Filter = leftFilter, - Projection = new Dictionary - { - [hasManyRelationship] = new(hasManyRelationship.RightType) - { - Filter = rightFilter, - Projection = new Dictionary - { - [rightIdAttribute] = null - } - }, - [leftIdAttribute] = null - } + Selection = primarySelection }; } @@ -518,22 +525,36 @@ protected virtual PaginationExpression GetPagination(IReadOnlyCollection? GetProjectionForSparseAttributeSet(ResourceType resourceType) +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection + protected virtual FieldSelection? GetSelectionForSparseAttributeSet(ResourceType resourceType) +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForQuery(resourceType); + var selection = new FieldSelection(); - if (!fieldSet.Any()) + HashSet resourceTypes = resourceType.GetAllConcreteDerivedTypes().ToHashSet(); + resourceTypes.Add(resourceType); + + foreach (ResourceType nextType in resourceTypes) { - return null; - } + IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForQuery(nextType); - HashSet attributeSet = fieldSet.OfType().ToHashSet(); - AttrAttribute idAttribute = GetIdAttribute(resourceType); - attributeSet.Add(idAttribute); + if (!fieldSet.Any()) + { + continue; + } + + HashSet attributeSet = fieldSet.OfType().ToHashSet(); + + FieldSelectors selectors = selection.GetOrCreateSelectors(nextType); + selectors.IncludeAttributes(attributeSet); + + AttrAttribute idAttribute = GetIdAttribute(nextType); + selectors.IncludeAttribute(idAttribute); + } - return attributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer?)null); + return selection.IsEmpty ? null : selection; } private static AttrAttribute GetIdAttribute(ResourceType resourceType) diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs index 6384600a58..14c37bb70d 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs @@ -37,44 +37,54 @@ public Expression ApplyInclude(IncludeExpression include) public override Expression VisitInclude(IncludeExpression expression, object? argument) { - Expression source = ApplyEagerLoads(_source, _resourceType.EagerLoads, null); + // De-duplicate chains coming from derived relationships. + HashSet propertyPaths = new(); + + ApplyEagerLoads(_resourceType.EagerLoads, null, propertyPaths); foreach (ResourceFieldChainExpression chain in IncludeChainConverter.GetRelationshipChains(expression)) { - source = ProcessRelationshipChain(chain, source); + ProcessRelationshipChain(chain, propertyPaths); } - return source; + return ToExpression(propertyPaths); } - private Expression ProcessRelationshipChain(ResourceFieldChainExpression chain, Expression source) + private void ProcessRelationshipChain(ResourceFieldChainExpression chain, ISet outputPropertyPaths) { string? path = null; - Expression result = source; foreach (RelationshipAttribute relationship in chain.Fields.Cast()) { path = path == null ? relationship.Property.Name : $"{path}.{relationship.Property.Name}"; - result = ApplyEagerLoads(result, relationship.RightType.EagerLoads, path); + ApplyEagerLoads(relationship.RightType.EagerLoads, path, outputPropertyPaths); } - return IncludeExtensionMethodCall(result, path!); + outputPropertyPaths.Add(path!); } - private Expression ApplyEagerLoads(Expression source, IEnumerable eagerLoads, string? pathPrefix) + private void ApplyEagerLoads(IEnumerable eagerLoads, string? pathPrefix, ISet outputPropertyPaths) { - Expression result = source; - foreach (EagerLoadAttribute eagerLoad in eagerLoads) { string path = pathPrefix != null ? $"{pathPrefix}.{eagerLoad.Property.Name}" : eagerLoad.Property.Name; - result = IncludeExtensionMethodCall(result, path); + outputPropertyPaths.Add(path); + + ApplyEagerLoads(eagerLoad.Children, path, outputPropertyPaths); + } + } + + private Expression ToExpression(HashSet propertyPaths) + { + Expression source = _source; - result = ApplyEagerLoads(result, eagerLoad.Children, path); + foreach (string propertyPath in propertyPaths) + { + source = IncludeExtensionMethodCall(source, propertyPath); } - return result; + return source; } private Expression IncludeExtensionMethodCall(Expression source, string navigationPropertyPath) diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScope.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScope.cs index 8be9e4263e..e5502031a3 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScope.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScope.cs @@ -14,15 +14,30 @@ public sealed class LambdaScope : IDisposable public ParameterExpression Parameter { get; } public Expression Accessor { get; } - public LambdaScope(LambdaParameterNameFactory nameFactory, Type elementType, Expression? accessorExpression) + private LambdaScope(LambdaParameterNameScope parameterNameScope, ParameterExpression parameter, Expression accessor) + { + _parameterNameScope = parameterNameScope; + Parameter = parameter; + Accessor = accessor; + } + + public static LambdaScope Create(LambdaParameterNameFactory nameFactory, Type elementType, Expression? accessorExpression) { ArgumentGuard.NotNull(nameFactory, nameof(nameFactory)); ArgumentGuard.NotNull(elementType, nameof(elementType)); - _parameterNameScope = nameFactory.Create(elementType.Name); - Parameter = Expression.Parameter(elementType, _parameterNameScope.Name); + LambdaParameterNameScope parameterNameScope = nameFactory.Create(elementType.Name); + ParameterExpression parameter = Expression.Parameter(elementType, parameterNameScope.Name); + Expression accessor = accessorExpression ?? parameter; + + return new LambdaScope(parameterNameScope, parameter, accessor); + } + + public LambdaScope WithAccessor(Expression accessorExpression) + { + ArgumentGuard.NotNull(accessorExpression, nameof(accessorExpression)); - Accessor = accessorExpression ?? Parameter; + return new LambdaScope(_parameterNameScope, Parameter, accessorExpression); } public void Dispose() diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScopeFactory.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScopeFactory.cs index 3ba7c5aab9..9c13a63d28 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScopeFactory.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScopeFactory.cs @@ -19,6 +19,6 @@ public LambdaScope CreateScope(Type elementType, Expression? accessorExpression { ArgumentGuard.NotNull(elementType, nameof(elementType)); - return new LambdaScope(_nameFactory, elementType, accessorExpression); + return LambdaScope.Create(_nameFactory, elementType, accessorExpression); } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs index bf14c70b6d..d04ff57e9d 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs @@ -1,6 +1,7 @@ using System.Linq.Expressions; using System.Reflection; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding; @@ -9,7 +10,7 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding; /// public abstract class QueryClauseBuilder : QueryExpressionVisitor { - protected LambdaScope LambdaScope { get; } + protected LambdaScope LambdaScope { get; private set; } protected QueryClauseBuilder(LambdaScope lambdaScope) { @@ -59,28 +60,48 @@ public override Expression VisitCount(CountExpression expression, TArgument argu } public override Expression VisitResourceFieldChain(ResourceFieldChainExpression expression, TArgument argument) - { - string[] components = expression.Fields.Select(field => field.Property.Name).ToArray(); - - return CreatePropertyExpressionFromComponents(LambdaScope.Accessor, components); - } - - private static MemberExpression CreatePropertyExpressionFromComponents(Expression source, IEnumerable components) { MemberExpression? property = null; - foreach (string propertyName in components) + foreach (ResourceFieldAttribute field in expression.Fields) { - Type parentType = property == null ? source.Type : property.Type; + Expression parentAccessor = property ?? LambdaScope.Accessor; + Type propertyType = field.Property.DeclaringType!; + string propertyName = field.Property.Name; + + bool requiresUpCast = parentAccessor.Type != propertyType && parentAccessor.Type.IsAssignableFrom(propertyType); + Type parentType = requiresUpCast ? propertyType : parentAccessor.Type; if (parentType.GetProperty(propertyName) == null) { 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 = requiresUpCast + ? Expression.MakeMemberAccess(Expression.Convert(parentAccessor, propertyType), field.Property) + : Expression.Property(parentAccessor, propertyName); } return property!; } + + protected TResult WithLambdaScopeAccessor(Expression accessorExpression, Func action) + { + ArgumentGuard.NotNull(accessorExpression, nameof(accessorExpression)); + ArgumentGuard.NotNull(action, nameof(action)); + + LambdaScope backupScope = LambdaScope; + + try + { + using (LambdaScope = LambdaScope.WithAccessor(accessorExpression)) + { + return action(); + } + } + finally + { + LambdaScope = backupScope; + } + } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs index 7ce50c02f1..d571ac1dce 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs @@ -3,7 +3,6 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore.Metadata; namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding; @@ -67,9 +66,9 @@ public virtual Expression ApplyQuery(QueryLayer layer) expression = ApplyPagination(expression, layer.Pagination); } - if (!layer.Projection.IsNullOrEmpty()) + if (layer.Selection is { IsEmpty: false }) { - expression = ApplyProjection(expression, layer.Projection, layer.ResourceType); + expression = ApplySelection(expression, layer.Selection, layer.ResourceType); } return expression; @@ -107,11 +106,11 @@ protected virtual Expression ApplyPagination(Expression source, PaginationExpres return builder.ApplySkipTake(pagination); } - protected virtual Expression ApplyProjection(Expression source, IDictionary projection, ResourceType resourceType) + protected virtual Expression ApplySelection(Expression source, FieldSelection selection, ResourceType resourceType) { using LambdaScope lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); var builder = new SelectClauseBuilder(source, lambdaScope, _entityModel, _extensionType, _nameFactory, _resourceFactory); - return builder.ApplySelect(projection, resourceType); + return builder.ApplySelect(selection, resourceType); } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs index 3931cdc180..690c49de24 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs @@ -16,6 +16,8 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding; [PublicAPI] public class SelectClauseBuilder : QueryClauseBuilder { + private static readonly MethodInfo TypeGetTypeMethod = typeof(object).GetMethod("GetType")!; + private static readonly MethodInfo TypeOpEqualityMethod = typeof(Type).GetMethod("op_Equality")!; private static readonly CollectionConverter CollectionConverter = new(); private static readonly ConstantExpression NullConstant = Expression.Constant(null); @@ -42,66 +44,111 @@ public SelectClauseBuilder(Expression source, LambdaScope lambdaScope, IModel en _resourceFactory = resourceFactory; } - public Expression ApplySelect(IDictionary selectors, ResourceType resourceType) + public Expression ApplySelect(FieldSelection selection, ResourceType resourceType) { - ArgumentGuard.NotNull(selectors, nameof(selectors)); + ArgumentGuard.NotNull(selection, nameof(selection)); - if (!selectors.Any()) - { - return _source; - } - - Expression bodyInitializer = CreateLambdaBodyInitializer(selectors, resourceType, LambdaScope, false); + Expression bodyInitializer = CreateLambdaBodyInitializer(selection, resourceType, LambdaScope, false); LambdaExpression lambda = Expression.Lambda(bodyInitializer, LambdaScope.Parameter); return SelectExtensionMethodCall(_source, LambdaScope.Parameter.Type, lambda); } - private Expression CreateLambdaBodyInitializer(IDictionary selectors, ResourceType resourceType, - LambdaScope lambdaScope, bool lambdaAccessorRequiresTestForNull) + private Expression CreateLambdaBodyInitializer(FieldSelection selection, ResourceType resourceType, LambdaScope lambdaScope, + bool lambdaAccessorRequiresTestForNull) { - ICollection propertySelectors = ToPropertySelectors(selectors, resourceType, lambdaScope.Accessor.Type); + IEntityType entityType = _entityModel.FindEntityType(resourceType.ClrType)!; + IEntityType[] concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToArray(); - MemberBinding[] propertyAssignments = - propertySelectors.Select(selector => CreatePropertyAssignment(selector, lambdaScope)).Cast().ToArray(); - - NewExpression newExpression = _resourceFactory.CreateNewExpression(lambdaScope.Accessor.Type); - Expression memberInit = Expression.MemberInit(newExpression, propertyAssignments); + Expression bodyInitializer = concreteEntityTypes.Length > 1 + ? CreateLambdaBodyInitializerForTypeHierarchy(selection, resourceType, concreteEntityTypes, lambdaScope) + : CreateLambdaBodyInitializerForSingleType(selection, resourceType, lambdaScope); if (!lambdaAccessorRequiresTestForNull) { - return memberInit; + return bodyInitializer; } - return TestForNull(lambdaScope.Accessor, memberInit); + return TestForNull(lambdaScope.Accessor, bodyInitializer); } - private ICollection ToPropertySelectors(IDictionary resourceFieldSelectors, - ResourceType resourceType, Type elementType) + private Expression CreateLambdaBodyInitializerForTypeHierarchy(FieldSelection selection, ResourceType baseResourceType, + IEnumerable concreteEntityTypes, LambdaScope lambdaScope) { - var propertySelectors = new Dictionary(); + IReadOnlySet resourceTypes = selection.GetResourceTypes(); + Expression rootCondition = lambdaScope.Accessor; + + foreach (IEntityType entityType in concreteEntityTypes) + { + ResourceType? resourceType = resourceTypes.SingleOrDefault(type => type.ClrType == entityType.ClrType); - // If a read-only attribute is selected, its calculated value likely depends on another property, so select all properties. - bool includesReadOnlyAttribute = resourceFieldSelectors.Any(selector => - selector.Key is AttrAttribute attribute && attribute.Property.SetMethod == null); + if (resourceType != null) + { + FieldSelectors fieldSelectors = selection.GetOrCreateSelectors(resourceType); - // Only selecting relationships implicitly means to select all attributes too. - bool containsOnlyRelationships = resourceFieldSelectors.All(selector => selector.Key is RelationshipAttribute); + if (!fieldSelectors.IsEmpty) + { + ICollection propertySelectors = ToPropertySelectors(fieldSelectors, resourceType, entityType.ClrType); - if (includesReadOnlyAttribute || containsOnlyRelationships) - { - IncludeAllProperties(elementType, propertySelectors); + MemberBinding[] propertyAssignments = propertySelectors.Select(selector => CreatePropertyAssignment(selector, lambdaScope)) + .Cast().ToArray(); + + NewExpression createInstance = _resourceFactory.CreateNewExpression(entityType.ClrType); + MemberInitExpression memberInit = Expression.MemberInit(createInstance, propertyAssignments); + UnaryExpression castToBaseType = Expression.Convert(memberInit, baseResourceType.ClrType); + + BinaryExpression typeCheck = CreateRuntimeTypeCheck(lambdaScope, entityType.ClrType); + rootCondition = Expression.Condition(typeCheck, castToBaseType, rootCondition); + } + } } - IncludeFieldSelection(resourceFieldSelectors, propertySelectors); + return rootCondition; + } + + private static BinaryExpression CreateRuntimeTypeCheck(LambdaScope lambdaScope, Type concreteClrType) + { + // Emitting "resource.GetType() == typeof(Article)" instead of "resource is Article" so we don't need to check for most-derived + // types first. This way, we can fallback to "anything else" at the end without worrying about order. + + Expression concreteTypeConstant = concreteClrType.CreateTupleAccessExpressionForConstant(typeof(Type)); + MethodCallExpression getTypeCall = Expression.Call(lambdaScope.Accessor, TypeGetTypeMethod); + return Expression.MakeBinary(ExpressionType.Equal, getTypeCall, concreteTypeConstant, false, TypeOpEqualityMethod); + } + + private Expression CreateLambdaBodyInitializerForSingleType(FieldSelection selection, ResourceType resourceType, LambdaScope lambdaScope) + { + FieldSelectors fieldSelectors = selection.GetOrCreateSelectors(resourceType); + ICollection propertySelectors = ToPropertySelectors(fieldSelectors, resourceType, lambdaScope.Accessor.Type); + + MemberBinding[] propertyAssignments = + propertySelectors.Select(selector => CreatePropertyAssignment(selector, lambdaScope)).Cast().ToArray(); + + NewExpression createInstance = _resourceFactory.CreateNewExpression(lambdaScope.Accessor.Type); + return Expression.MemberInit(createInstance, propertyAssignments); + } + + private ICollection ToPropertySelectors(FieldSelectors fieldSelectors, ResourceType resourceType, Type elementType) + { + var propertySelectors = new Dictionary(); + + if (fieldSelectors.ContainsReadOnlyAttribute || fieldSelectors.ContainsOnlyRelationships) + { + // If a read-only attribute is selected, its calculated value likely depends on another property, so select all properties. + // And only selecting relationships implicitly means to select all attributes too. + + IncludeAllAttributes(elementType, propertySelectors); + } + + IncludeFields(fieldSelectors, propertySelectors); IncludeEagerLoads(resourceType, propertySelectors); return propertySelectors.Values; } - private void IncludeAllProperties(Type elementType, Dictionary propertySelectors) + private void IncludeAllAttributes(Type elementType, Dictionary propertySelectors) { IEntityType entityModel = _entityModel.GetEntityTypes().Single(type => type.ClrType == elementType); IEnumerable entityProperties = entityModel.GetProperties().Where(property => !property.IsShadowProperty()).ToArray(); @@ -113,10 +160,9 @@ private void IncludeAllProperties(Type elementType, Dictionary resourceFieldSelectors, - Dictionary propertySelectors) + private static void IncludeFields(FieldSelectors fieldSelectors, Dictionary propertySelectors) { - foreach ((ResourceFieldAttribute resourceField, QueryLayer? queryLayer) in resourceFieldSelectors) + foreach ((ResourceFieldAttribute resourceField, QueryLayer? queryLayer) in fieldSelectors) { var propertySelector = new PropertySelector(resourceField.Property, queryLayer); IncludeWritableProperty(propertySelector, propertySelectors); @@ -146,21 +192,26 @@ private static void IncludeEagerLoads(ResourceType resourceType, Dictionary Visit(expression.Child, argument)); + + return Expression.AndAlso(typeCheck, filter); + } + public override Expression VisitMatchText(MatchTextExpression expression, Type? argument) { Expression property = Visit(expression.TargetAttribute, argument); diff --git a/src/JsonApiDotNetCore/Queries/QueryLayer.cs b/src/JsonApiDotNetCore/Queries/QueryLayer.cs index be1d657cfe..c460560a33 100644 --- a/src/JsonApiDotNetCore/Queries/QueryLayer.cs +++ b/src/JsonApiDotNetCore/Queries/QueryLayer.cs @@ -2,7 +2,6 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries; @@ -18,7 +17,7 @@ public sealed class QueryLayer public FilterExpression? Filter { get; set; } public SortExpression? Sort { get; set; } public PaginationExpression? Pagination { get; set; } - public IDictionary? Projection { get; set; } + public FieldSelection? Selection { get; set; } public QueryLayer(ResourceType resourceType) { @@ -32,92 +31,41 @@ public override string ToString() var builder = new StringBuilder(); var writer = new IndentingStringWriter(builder); - WriteLayer(writer, this); + WriteLayer(writer, null); return builder.ToString(); } - private static void WriteLayer(IndentingStringWriter writer, QueryLayer layer, string? prefix = null) + internal void WriteLayer(IndentingStringWriter writer, string? prefix) { - writer.WriteLine($"{prefix}{nameof(QueryLayer)}<{layer.ResourceType.ClrType.Name}>"); + writer.WriteLine($"{prefix}{nameof(QueryLayer)}<{ResourceType.ClrType.Name}>"); using (writer.Indent()) { - if (layer.Include != null) + if (Include != null) { - writer.WriteLine($"{nameof(Include)}: {layer.Include}"); + writer.WriteLine($"{nameof(Include)}: {Include}"); } - if (layer.Filter != null) + if (Filter != null) { - writer.WriteLine($"{nameof(Filter)}: {layer.Filter}"); + writer.WriteLine($"{nameof(Filter)}: {Filter}"); } - if (layer.Sort != null) + if (Sort != null) { - writer.WriteLine($"{nameof(Sort)}: {layer.Sort}"); + writer.WriteLine($"{nameof(Sort)}: {Sort}"); } - if (layer.Pagination != null) + if (Pagination != null) { - writer.WriteLine($"{nameof(Pagination)}: {layer.Pagination}"); + writer.WriteLine($"{nameof(Pagination)}: {Pagination}"); } - if (!layer.Projection.IsNullOrEmpty()) + if (Selection is { IsEmpty: false }) { - writer.WriteLine(nameof(Projection)); - - using (writer.Indent()) - { - foreach ((ResourceFieldAttribute field, QueryLayer? nextLayer) in layer.Projection) - { - if (nextLayer == null) - { - writer.WriteLine(field.ToString()); - } - else - { - WriteLayer(writer, nextLayer, $"{field.PublicName}: "); - } - } - } - } - } - } - - private sealed class IndentingStringWriter : IDisposable - { - private readonly StringBuilder _builder; - private int _indentDepth; - - public IndentingStringWriter(StringBuilder builder) - { - _builder = builder; - } - - public void WriteLine(string? line) - { - if (_indentDepth > 0) - { - _builder.Append(new string(' ', _indentDepth * 2)); - } - - _builder.AppendLine(line); - } - - public IndentingStringWriter Indent() - { - WriteLine("{"); - _indentDepth++; - return this; - } - - public void Dispose() - { - if (_indentDepth > 0) - { - _indentDepth--; - WriteLine("}"); + writer.WriteLine(nameof(Selection)); + Selection.WriteSelection(writer); } } } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs index 51b815c2ef..6c8bfa2934 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs @@ -6,7 +6,6 @@ using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Queries.Internal.Parsing; -using JsonApiDotNetCore.Resources.Annotations; using Microsoft.Extensions.Primitives; namespace JsonApiDotNetCore.QueryStrings.Internal; @@ -18,7 +17,6 @@ public class IncludeQueryStringParameterReader : QueryStringParameterReader, IIn private readonly IncludeParser _includeParser; private IncludeExpression? _includeExpression; - private string? _lastParameterName; public bool AllowEmptyValue => false; @@ -28,18 +26,7 @@ public IncludeQueryStringParameterReader(IJsonApiRequest request, IResourceGraph ArgumentGuard.NotNull(options, nameof(options)); _options = options; - _includeParser = new IncludeParser(ValidateSingleRelationship); - } - - protected void ValidateSingleRelationship(RelationshipAttribute relationship, ResourceType resourceType, string path) - { - if (!relationship.CanInclude) - { - throw new InvalidQueryStringParameterException(_lastParameterName!, "Including the requested relationship is not allowed.", - path == relationship.PublicName - ? $"Including the relationship '{relationship.PublicName}' on '{resourceType.PublicName}' is not allowed." - : $"Including the relationship '{relationship.PublicName}' in '{path}' on '{resourceType.PublicName}' is not allowed."); - } + _includeParser = new IncludeParser(); } /// @@ -59,8 +46,6 @@ public virtual bool CanRead(string parameterName) /// public virtual void Read(string parameterName, StringValues parameterValue) { - _lastParameterName = parameterName; - try { _includeExpression = GetInclude(parameterValue); diff --git a/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs b/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs index e9a9488b7b..cadbd658a8 100644 --- a/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs +++ b/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs @@ -35,7 +35,7 @@ public static IIdentifiable GetTrackedOrAttach(this DbContext dbContext, IIdenti ArgumentGuard.NotNull(dbContext, nameof(dbContext)); ArgumentGuard.NotNull(identifiable, nameof(identifiable)); - Type resourceClrType = identifiable.GetType(); + Type resourceClrType = identifiable.GetClrType(); string? stringId = identifiable.StringId; EntityEntry? entityEntry = dbContext.ChangeTracker.Entries().FirstOrDefault(entry => IsResource(entry, resourceClrType, stringId)); diff --git a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs index 52c07c5244..7384cec693 100644 --- a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs @@ -105,7 +105,9 @@ public virtual async Task CountAsync(FilterExpression? filter, Cancellation } } +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection protected virtual IQueryable ApplyQueryLayer(QueryLayer queryLayer) +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection { _traceWriter.LogMethodStart(new { @@ -149,20 +151,23 @@ protected virtual IQueryable ApplyQueryLayer(QueryLayer queryLayer) } } +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection protected virtual IQueryable GetAll() +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection { return _dbContext.Set(); } /// - public virtual Task GetForCreateAsync(TId id, CancellationToken cancellationToken) + public virtual Task GetForCreateAsync(Type resourceClrType, TId id, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new { + resourceClrType, id }); - var resource = _resourceFactory.CreateInstance(); + var resource = (TResource)_resourceFactory.CreateInstance(resourceClrType); resource.Id = id; return Task.FromResult(resource); @@ -220,12 +225,12 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r if (relationship is HasManyAttribute hasManyRelationship) { - HashSet rightResourceIdSet = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); + HashSet rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); - await _resourceDefinitionAccessor.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIdSet, writeOperation, + await _resourceDefinitionAccessor.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIds, writeOperation, cancellationToken); - return rightResourceIdSet; + return rightResourceIds; } return rightValue; @@ -305,10 +310,11 @@ protected void AssertIsNotClearingRequiredToOneRelationship(RelationshipAttribut } /// - public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToken) + public virtual async Task DeleteAsync(TResource? resourceFromDatabase, TId id, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new { + resourceFromDatabase, id }); @@ -316,7 +322,7 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke // This enables OnWritingAsync() to fetch the resource, which adds it to the change tracker. // If so, we'll reuse the tracked resource instead of this placeholder resource. - var placeholderResource = _resourceFactory.CreateInstance(); + TResource placeholderResource = resourceFromDatabase ?? _resourceFactory.CreateInstance(); placeholderResource.Id = id; await _resourceDefinitionAccessor.OnWritingAsync(placeholderResource, WriteOperationKind.DeleteResource, cancellationToken); @@ -413,10 +419,12 @@ public virtual async Task SetRelationshipAsync(TResource leftResource, object? r } /// - public virtual async Task AddToToManyRelationshipAsync(TId leftId, ISet rightResourceIds, CancellationToken cancellationToken) + public virtual async Task AddToToManyRelationshipAsync(TResource? leftResource, TId leftId, ISet rightResourceIds, + CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new { + leftResource, leftId, rightResourceIds }); @@ -427,18 +435,22 @@ public virtual async Task AddToToManyRelationshipAsync(TId leftId, ISet(leftId, relationship, rightResourceIds, cancellationToken); + // This enables OnAddToRelationshipAsync() or OnWritingAsync() to fetch the resource, which adds it to the change tracker. + // If so, we'll reuse the tracked resource instead of this placeholder resource. + TResource leftPlaceholderResource = leftResource ?? _resourceFactory.CreateInstance(); + leftPlaceholderResource.Id = leftId; + + await _resourceDefinitionAccessor.OnAddToRelationshipAsync(leftPlaceholderResource, relationship, rightResourceIds, cancellationToken); if (rightResourceIds.Any()) { - var leftPlaceholderResource = _resourceFactory.CreateInstance(); - leftPlaceholderResource.Id = leftId; - var leftResourceTracked = (TResource)_dbContext.GetTrackedOrAttach(leftPlaceholderResource); + IEnumerable rightValueToStore = GetRightValueToStoreForAddToToMany(leftResourceTracked, relationship, rightResourceIds); - await UpdateRelationshipAsync(relationship, leftResourceTracked, rightResourceIds, cancellationToken); + await UpdateRelationshipAsync(relationship, leftResourceTracked, rightValueToStore, cancellationToken); await _resourceDefinitionAccessor.OnWritingAsync(leftResourceTracked, WriteOperationKind.AddToRelationship, cancellationToken); + leftResourceTracked = (TResource)_dbContext.GetTrackedOrAttach(leftResourceTracked); await SaveChangesAsync(cancellationToken); @@ -446,6 +458,30 @@ public virtual async Task AddToToManyRelationshipAsync(TId leftId, ISet rightResourceIdsToAdd) + { + object? rightValueStored = relationship.GetValue(leftResource); + + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + + HashSet rightResourceIdsStored = _collectionConverter + .ExtractResources(rightValueStored) + .Select(rightResource => _dbContext.GetTrackedOrAttach(rightResource)) + .ToHashSet(IdentifiableComparer.Instance); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + + if (rightResourceIdsStored.Any()) + { + rightResourceIdsStored.AddRange(rightResourceIdsToAdd); + return rightResourceIdsStored; + } + + return rightResourceIdsToAdd; + } + /// public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResource, ISet rightResourceIds, CancellationToken cancellationToken) @@ -473,7 +509,7 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour // Make Entity Framework Core believe any additional resources added from ResourceDefinition already exist in database. IIdentifiable[] extraResourceIdsToRemove = rightResourceIdsToRemove.Where(rightId => !rightResourceIds.Contains(rightId)).ToArray(); - object? rightValueStored = relationship.GetValue(leftResource); + object? rightValueStored = relationship.GetValue(leftResourceTracked); // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true @@ -488,9 +524,9 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour // @formatter:wrap_chained_method_calls restore rightValueStored = _collectionConverter.CopyToTypedCollection(rightResourceIdsStored, relationship.Property.PropertyType); - relationship.SetValue(leftResource, rightValueStored); + relationship.SetValue(leftResourceTracked, rightValueStored); - MarkRelationshipAsLoaded(leftResource, relationship); + MarkRelationshipAsLoaded(leftResourceTracked, relationship); HashSet rightResourceIdsToStore = rightResourceIdsStored.ToHashSet(IdentifiableComparer.Instance); rightResourceIdsToStore.ExceptWith(rightResourceIdsToRemove); @@ -540,7 +576,7 @@ protected async Task UpdateRelationshipAsync(RelationshipAttribute relationship, return null; } - ICollection rightResources = _collectionConverter.ExtractResources(rightValue); + IReadOnlyCollection rightResources = _collectionConverter.ExtractResources(rightValue); IIdentifiable[] rightResourcesTracked = rightResources.Select(rightResource => _dbContext.GetTrackedOrAttach(rightResource)).ToArray(); return rightValue is IEnumerable diff --git a/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs index 9654365121..149fef1cfb 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Repositories; public interface IResourceRepositoryAccessor { /// - /// Invokes . + /// Invokes for the specified resource type. /// Task> GetAsync(QueryLayer queryLayer, CancellationToken cancellationToken) where TResource : class, IIdentifiable; @@ -27,25 +27,25 @@ Task> GetAsync(QueryLayer queryLayer, Task CountAsync(ResourceType resourceType, FilterExpression? filter, CancellationToken cancellationToken); /// - /// Invokes . + /// Invokes for the specified resource type. /// - Task GetForCreateAsync(TId id, CancellationToken cancellationToken) + Task GetForCreateAsync(Type resourceClrType, TId id, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes . + /// Invokes for the specified resource type. /// Task CreateAsync(TResource resourceFromRequest, TResource resourceForDatabase, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes . + /// Invokes for the specified resource type. /// Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes . + /// Invokes for the specified resource type. /// Task UpdateAsync(TResource resourceFromRequest, TResource resourceFromDatabase, CancellationToken cancellationToken) where TResource : class, IIdentifiable; @@ -53,11 +53,11 @@ Task UpdateAsync(TResource resourceFromRequest, TResource resourceFro /// /// Invokes for the specified resource type. /// - Task DeleteAsync(TId id, CancellationToken cancellationToken) + Task DeleteAsync(TResource? resourceFromDatabase, TId id, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes . + /// Invokes for the specified resource type. /// Task SetRelationshipAsync(TResource leftResource, object? rightValue, CancellationToken cancellationToken) where TResource : class, IIdentifiable; @@ -65,11 +65,12 @@ Task SetRelationshipAsync(TResource leftResource, object? rightValue, /// /// Invokes for the specified resource type. /// - Task AddToToManyRelationshipAsync(TId leftId, ISet rightResourceIds, CancellationToken cancellationToken) + Task AddToToManyRelationshipAsync(TResource? leftResource, TId leftId, ISet rightResourceIds, + CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes . + /// Invokes for the specified resource type. /// Task RemoveFromToManyRelationshipAsync(TResource leftResource, ISet rightResourceIds, CancellationToken cancellationToken) where TResource : class, IIdentifiable; diff --git a/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs b/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs index ca0186d5b5..fb0267d18a 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs @@ -23,7 +23,7 @@ public interface IResourceWriteRepository /// /// This method can be overridden to assign resource-specific required relationships. /// - Task GetForCreateAsync(TId id, CancellationToken cancellationToken); + Task GetForCreateAsync(Type resourceClrType, TId id, CancellationToken cancellationToken); /// /// Creates a new resource in the underlying data store. @@ -43,7 +43,7 @@ public interface IResourceWriteRepository /// /// Deletes an existing resource from the underlying data store. /// - Task DeleteAsync(TId id, CancellationToken cancellationToken); + Task DeleteAsync(TResource? resourceFromDatabase, TId id, CancellationToken cancellationToken); /// /// Performs a complete replacement of the relationship in the underlying data store. @@ -53,7 +53,7 @@ public interface IResourceWriteRepository /// /// Adds resources to a to-many relationship in the underlying data store. /// - Task AddToToManyRelationshipAsync(TId leftId, ISet rightResourceIds, CancellationToken cancellationToken); + Task AddToToManyRelationshipAsync(TResource? leftResource, TId leftId, ISet rightResourceIds, CancellationToken cancellationToken); /// /// Removes resources from a to-many relationship in the underlying data store. diff --git a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs index 0ba96126a1..5f00cdf08d 100644 --- a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs @@ -53,11 +53,11 @@ public async Task CountAsync(ResourceType resourceType, FilterExpression? f } /// - public async Task GetForCreateAsync(TId id, CancellationToken cancellationToken) + public async Task GetForCreateAsync(Type resourceClrType, TId id, CancellationToken cancellationToken) where TResource : class, IIdentifiable { dynamic repository = GetWriteRepository(typeof(TResource)); - return await repository.GetForCreateAsync(id, cancellationToken); + return await repository.GetForCreateAsync(resourceClrType, id, cancellationToken); } /// @@ -85,11 +85,11 @@ public async Task UpdateAsync(TResource resourceFromRequest, TResourc } /// - public async Task DeleteAsync(TId id, CancellationToken cancellationToken) + public async Task DeleteAsync(TResource? resourceFromDatabase, TId id, CancellationToken cancellationToken) where TResource : class, IIdentifiable { dynamic repository = GetWriteRepository(typeof(TResource)); - await repository.DeleteAsync(id, cancellationToken); + await repository.DeleteAsync(resourceFromDatabase, id, cancellationToken); } /// @@ -101,11 +101,12 @@ public async Task SetRelationshipAsync(TResource leftResource, object } /// - public async Task AddToToManyRelationshipAsync(TId leftId, ISet rightResourceIds, CancellationToken cancellationToken) + public async Task AddToToManyRelationshipAsync(TResource? leftResource, TId leftId, ISet rightResourceIds, + CancellationToken cancellationToken) where TResource : class, IIdentifiable { dynamic repository = GetWriteRepository(typeof(TResource)); - await repository.AddToToManyRelationshipAsync(leftId, rightResourceIds, cancellationToken); + await repository.AddToToManyRelationshipAsync(leftResource, leftId, rightResourceIds, cancellationToken); } /// diff --git a/src/JsonApiDotNetCore/Resources/AbstractResourceWrapper.cs b/src/JsonApiDotNetCore/Resources/AbstractResourceWrapper.cs new file mode 100644 index 0000000000..0d294feb57 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/AbstractResourceWrapper.cs @@ -0,0 +1,15 @@ +namespace JsonApiDotNetCore.Resources; + +/// +internal sealed class AbstractResourceWrapper : Identifiable, IAbstractResourceWrapper +{ + /// + public Type AbstractType { get; } + + public AbstractResourceWrapper(Type abstractType) + { + ArgumentGuard.NotNull(abstractType, nameof(abstractType)); + + AbstractType = abstractType; + } +} diff --git a/src/JsonApiDotNetCore/Resources/IAbstractResourceWrapper.cs b/src/JsonApiDotNetCore/Resources/IAbstractResourceWrapper.cs new file mode 100644 index 0000000000..7e9ac3c71d --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/IAbstractResourceWrapper.cs @@ -0,0 +1,12 @@ +namespace JsonApiDotNetCore.Resources; + +/// +/// Because an instance cannot be created from an abstract resource type, this wrapper is used to preserve that information. +/// +internal interface IAbstractResourceWrapper : IIdentifiable +{ + /// + /// The abstract resource type. + /// + Type AbstractType { get; } +} diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs index 1a61868de5..9af79831b2 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs @@ -108,14 +108,16 @@ public interface IResourceDefinition /// } /// ]]> /// -#pragma warning disable AV1130 // Return type in method signature should be a collection interface instead of a concrete type +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection QueryStringParameterHandlers? OnRegisterQueryableHandlersForQueryStringParameters(); -#pragma warning restore AV1130 // Return type in method signature should be a collection interface instead of a concrete type +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection /// /// Enables to add JSON:API meta information, specific to this resource. /// +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection IDictionary? GetMeta(TResource resource); +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection /// /// Executes after the original version of the resource has been retrieved from the underlying data store, as part of a write request. @@ -133,9 +135,9 @@ public interface IResourceDefinition /// /// /// Identifies the logical write operation for which this method was called. Possible values: , - /// and . Note this intentionally excludes - /// , and - /// , because for those endpoints no resource is retrieved upfront. + /// , and + /// . Note this intentionally excludes and + /// , because for those endpoints no resource is retrieved upfront. /// /// /// Propagates notification that request handling should be canceled. @@ -203,9 +205,22 @@ Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasMa /// Implementing this method enables to perform validations and make changes to , before the relationship is updated. /// /// - /// + /// /// Identifier of the left resource. The indication "left" specifies that is declared on - /// . + /// . In contrast to other relationship methods, this value is not retrieved from the underlying data store, except in + /// the following two cases: + /// + /// + /// + /// is a many-to-many relationship. This is required to prevent failure when already assigned. + /// + /// + /// + /// + /// The left resource type is part of a type hierarchy. This ensures your business logic runs against the actually stored type. + /// + /// + /// /// /// /// The to-many relationship being added to. @@ -216,7 +231,7 @@ Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasMa /// /// Propagates notification that request handling should be canceled. /// - Task OnAddToRelationshipAsync(TId leftResourceId, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + Task OnAddToRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, CancellationToken cancellationToken); /// diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs index a6d229609a..55f32ead40 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs @@ -45,7 +45,9 @@ public interface IResourceDefinitionAccessor /// /// Invokes for the specified resource. /// +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection IDictionary? GetMeta(ResourceType resourceType, IIdentifiable resourceInstance); +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection /// /// Invokes for the specified resource. @@ -70,9 +72,9 @@ public Task OnSetToManyRelationshipAsync(TResource leftResource, HasM /// /// Invokes for the specified resource. /// - public Task OnAddToRelationshipAsync(TId leftResourceId, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + public Task OnAddToRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, CancellationToken cancellationToken) - where TResource : class, IIdentifiable; + where TResource : class, IIdentifiable; /// /// Invokes for the specified resource. diff --git a/src/JsonApiDotNetCore/Resources/IdentifiableComparer.cs b/src/JsonApiDotNetCore/Resources/IdentifiableComparer.cs index a59dc8f15f..8a6bed2f91 100644 --- a/src/JsonApiDotNetCore/Resources/IdentifiableComparer.cs +++ b/src/JsonApiDotNetCore/Resources/IdentifiableComparer.cs @@ -22,7 +22,7 @@ public bool Equals(IIdentifiable? left, IIdentifiable? right) return true; } - if (left is null || right is null || left.GetType() != right.GetType()) + if (left is null || right is null || left.GetClrType() != right.GetClrType()) { return false; } @@ -38,6 +38,6 @@ public bool Equals(IIdentifiable? left, IIdentifiable? right) public int GetHashCode(IIdentifiable obj) { // LocalId is intentionally omitted here, it is okay for hashes to collide. - return HashCode.Combine(obj.GetType(), obj.StringId); + return HashCode.Combine(obj.GetClrType(), obj.StringId); } } diff --git a/src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs b/src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs index 7e8064826a..8c9d4b6a36 100644 --- a/src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs +++ b/src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs @@ -11,11 +11,11 @@ public static object GetTypedId(this IIdentifiable identifiable) { ArgumentGuard.NotNull(identifiable, nameof(identifiable)); - PropertyInfo? property = identifiable.GetType().GetProperty(IdPropertyName); + PropertyInfo? property = identifiable.GetClrType().GetProperty(IdPropertyName); if (property == null) { - throw new InvalidOperationException($"Resource of type '{identifiable.GetType()}' does not contain a property named '{IdPropertyName}'."); + throw new InvalidOperationException($"Resource of type '{identifiable.GetClrType()}' does not contain a property named '{IdPropertyName}'."); } object? propertyValue = property.GetValue(identifiable); @@ -27,11 +27,18 @@ public static object GetTypedId(this IIdentifiable identifiable) if (Equals(propertyValue, defaultValue)) { - throw new InvalidOperationException($"Property '{identifiable.GetType().Name}.{IdPropertyName}' should " + + throw new InvalidOperationException($"Property '{identifiable.GetClrType().Name}.{IdPropertyName}' should " + $"have been assigned at this point, but it contains its default {property.PropertyType.Name} value '{propertyValue}'."); } } return propertyValue!; } + + public static Type GetClrType(this IIdentifiable identifiable) + { + ArgumentGuard.NotNull(identifiable, nameof(identifiable)); + + return identifiable is IAbstractResourceWrapper abstractResource ? abstractResource.AbstractType : identifiable.GetType(); + } } diff --git a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs index 99f575a293..dbb90bf6fe 100644 --- a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs @@ -1,11 +1,14 @@ using System.Collections.Immutable; using System.ComponentModel; using System.Linq.Expressions; +using System.Net; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Resources; @@ -54,8 +57,9 @@ public virtual IImmutableSet OnApplyIncludes(IImmutabl /// model.CreatedAt, ListSortDirection.Ascending), - /// (model => model.Password, ListSortDirection.Descending) + /// (blog => blog.Author.Name.LastName, ListSortDirection.Ascending), + /// (blog => blog.Posts.Count, ListSortDirection.Descending), + /// (blog => blog.Title, ListSortDirection.Ascending) /// }); /// ]]> /// @@ -64,14 +68,26 @@ protected SortExpression CreateSortExpressionFromLambda(PropertySortOrder keySel ArgumentGuard.NotNullNorEmpty(keySelectors, nameof(keySelectors)); ImmutableArray.Builder elementsBuilder = ImmutableArray.CreateBuilder(keySelectors.Count); + var lambdaConverter = new SortExpressionLambdaConverter(ResourceGraph); - foreach ((Expression> keySelector, ListSortDirection sortDirection) in keySelectors) + foreach ((Expression> keySelector, ListSortDirection sortDirection) in keySelectors) { - bool isAscending = sortDirection == ListSortDirection.Ascending; - AttrAttribute attribute = ResourceGraph.GetAttributes(keySelector).Single(); - - var sortElement = new SortElementExpression(new ResourceFieldChainExpression(attribute), isAscending); - elementsBuilder.Add(sortElement); + try + { + SortElementExpression sortElement = lambdaConverter.FromLambda(keySelector, sortDirection); + elementsBuilder.Add(sortElement); + } + catch (InvalidOperationException exception) + { + throw new JsonApiException(new ErrorObject(HttpStatusCode.InternalServerError) + { + Title = "Invalid lambda expression for sorting from resource definition. " + + "It should select a property that is exposed as an attribute, or a to-many relationship followed by Count(). " + + "The property can be preceded by a path of to-one relationships. " + + "Examples: 'blog => blog.Title', 'blog => blog.Posts.Count', 'blog => blog.Author.Name.LastName'.", + Detail = $"The lambda expression '{keySelector}' is invalid. {exception.Message}" + }, exception); + } } return new SortExpression(elementsBuilder.ToImmutable()); @@ -122,7 +138,7 @@ public virtual Task OnSetToManyRelationshipAsync(TResource leftResource, HasMany } /// - public virtual Task OnAddToRelationshipAsync(TId leftResourceId, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + public virtual Task OnAddToRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, CancellationToken cancellationToken) { return Task.CompletedTask; @@ -161,7 +177,7 @@ public virtual void OnSerialize(TResource resource) /// 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)> + public sealed class PropertySortOrder : List<(Expression> KeySelector, ListSortDirection SortDirection)> { } } diff --git a/src/JsonApiDotNetCore/Resources/OperationContainer.cs b/src/JsonApiDotNetCore/Resources/OperationContainer.cs index 91f2553fd2..d2fa2c0d3e 100644 --- a/src/JsonApiDotNetCore/Resources/OperationContainer.cs +++ b/src/JsonApiDotNetCore/Resources/OperationContainer.cs @@ -39,7 +39,9 @@ public OperationContainer WithResource(IIdentifiable resource) return new OperationContainer(resource, TargetedFields, Request); } +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection public ISet GetSecondaryResources() +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection { var secondaryResources = new HashSet(IdentifiableComparer.Instance); @@ -54,7 +56,7 @@ public ISet GetSecondaryResources() private void AddSecondaryResources(RelationshipAttribute relationship, HashSet secondaryResources) { object? rightValue = relationship.GetValue(Resource); - ICollection rightResources = CollectionConverter.ExtractResources(rightValue); + IReadOnlyCollection rightResources = CollectionConverter.ExtractResources(rightValue); secondaryResources.AddRange(rightResources); } diff --git a/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs index 7ccd381456..658e5e2c5b 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs @@ -1,6 +1,6 @@ using System.Text.Json; using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Resources; @@ -10,19 +10,19 @@ namespace JsonApiDotNetCore.Resources; public sealed class ResourceChangeTracker : IResourceChangeTracker where TResource : class, IIdentifiable { - private readonly ResourceType _resourceType; + private readonly IJsonApiRequest _request; private readonly ITargetedFields _targetedFields; private IDictionary? _initiallyStoredAttributeValues; private IDictionary? _requestAttributeValues; private IDictionary? _finallyStoredAttributeValues; - public ResourceChangeTracker(IResourceGraph resourceGraph, ITargetedFields targetedFields) + public ResourceChangeTracker(IJsonApiRequest request, ITargetedFields targetedFields) { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); - _resourceType = resourceGraph.GetResourceType(); + _request = request; _targetedFields = targetedFields; } @@ -31,7 +31,7 @@ public void SetInitiallyStoredAttributeValues(TResource resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - _initiallyStoredAttributeValues = CreateAttributeDictionary(resource, _resourceType.Attributes); + _initiallyStoredAttributeValues = CreateAttributeDictionary(resource, _request.PrimaryResourceType!.Attributes); } /// @@ -47,7 +47,7 @@ public void SetFinallyStoredAttributeValues(TResource resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - _finallyStoredAttributeValues = CreateAttributeDictionary(resource, _resourceType.Attributes); + _finallyStoredAttributeValues = CreateAttributeDictionary(resource, _request.PrimaryResourceType!.Attributes); } private IDictionary CreateAttributeDictionary(TResource resource, IEnumerable attributes) diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs index a16b04cfea..9f8dfdddeb 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -104,8 +104,8 @@ public async Task OnPrepareWriteAsync(TResource resource, WriteOperat { ArgumentGuard.NotNull(resource, nameof(resource)); - dynamic resourceDefinition = ResolveResourceDefinition(typeof(TResource)); - await resourceDefinition.OnPrepareWriteAsync(resource, writeOperation, cancellationToken); + dynamic resourceDefinition = ResolveResourceDefinition(resource.GetClrType()); + await resourceDefinition.OnPrepareWriteAsync((dynamic)resource, writeOperation, cancellationToken); } /// @@ -116,8 +116,10 @@ public async Task OnPrepareWriteAsync(TResource resource, WriteOperat ArgumentGuard.NotNull(leftResource, nameof(leftResource)); ArgumentGuard.NotNull(hasOneRelationship, nameof(hasOneRelationship)); - dynamic resourceDefinition = ResolveResourceDefinition(typeof(TResource)); - return await resourceDefinition.OnSetToOneRelationshipAsync(leftResource, hasOneRelationship, rightResourceId, writeOperation, cancellationToken); + dynamic resourceDefinition = ResolveResourceDefinition(leftResource.GetClrType()); + + return await resourceDefinition.OnSetToOneRelationshipAsync((dynamic)leftResource, hasOneRelationship, rightResourceId, writeOperation, + cancellationToken); } /// @@ -129,20 +131,20 @@ public async Task OnSetToManyRelationshipAsync(TResource leftResource ArgumentGuard.NotNull(hasManyRelationship, nameof(hasManyRelationship)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); - dynamic resourceDefinition = ResolveResourceDefinition(typeof(TResource)); - await resourceDefinition.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIds, writeOperation, cancellationToken); + dynamic resourceDefinition = ResolveResourceDefinition(leftResource.GetClrType()); + await resourceDefinition.OnSetToManyRelationshipAsync((dynamic)leftResource, hasManyRelationship, rightResourceIds, writeOperation, cancellationToken); } /// - public async Task OnAddToRelationshipAsync(TId leftResourceId, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + public async Task OnAddToRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, CancellationToken cancellationToken) - where TResource : class, IIdentifiable + where TResource : class, IIdentifiable { ArgumentGuard.NotNull(hasManyRelationship, nameof(hasManyRelationship)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); - dynamic resourceDefinition = ResolveResourceDefinition(typeof(TResource)); - await resourceDefinition.OnAddToRelationshipAsync(leftResourceId, hasManyRelationship, rightResourceIds, cancellationToken); + dynamic resourceDefinition = ResolveResourceDefinition(leftResource.GetClrType()); + await resourceDefinition.OnAddToRelationshipAsync((dynamic)leftResource, hasManyRelationship, rightResourceIds, cancellationToken); } /// @@ -154,8 +156,8 @@ public async Task OnRemoveFromRelationshipAsync(TResource leftResourc ArgumentGuard.NotNull(hasManyRelationship, nameof(hasManyRelationship)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); - dynamic resourceDefinition = ResolveResourceDefinition(typeof(TResource)); - await resourceDefinition.OnRemoveFromRelationshipAsync(leftResource, hasManyRelationship, rightResourceIds, cancellationToken); + dynamic resourceDefinition = ResolveResourceDefinition(leftResource.GetClrType()); + await resourceDefinition.OnRemoveFromRelationshipAsync((dynamic)leftResource, hasManyRelationship, rightResourceIds, cancellationToken); } /// @@ -164,8 +166,8 @@ public async Task OnWritingAsync(TResource resource, WriteOperationKi { ArgumentGuard.NotNull(resource, nameof(resource)); - dynamic resourceDefinition = ResolveResourceDefinition(typeof(TResource)); - await resourceDefinition.OnWritingAsync(resource, writeOperation, cancellationToken); + dynamic resourceDefinition = ResolveResourceDefinition(resource.GetClrType()); + await resourceDefinition.OnWritingAsync((dynamic)resource, writeOperation, cancellationToken); } /// @@ -174,8 +176,8 @@ public async Task OnWriteSucceededAsync(TResource resource, WriteOper { ArgumentGuard.NotNull(resource, nameof(resource)); - dynamic resourceDefinition = ResolveResourceDefinition(typeof(TResource)); - await resourceDefinition.OnWriteSucceededAsync(resource, writeOperation, cancellationToken); + dynamic resourceDefinition = ResolveResourceDefinition(resource.GetClrType()); + await resourceDefinition.OnWriteSucceededAsync((dynamic)resource, writeOperation, cancellationToken); } /// @@ -183,7 +185,7 @@ public void OnDeserialize(IIdentifiable resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - dynamic resourceDefinition = ResolveResourceDefinition(resource.GetType()); + dynamic resourceDefinition = ResolveResourceDefinition(resource.GetClrType()); resourceDefinition.OnDeserialize((dynamic)resource); } @@ -192,7 +194,7 @@ public void OnSerialize(IIdentifiable resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - dynamic resourceDefinition = ResolveResourceDefinition(resource.GetType()); + dynamic resourceDefinition = ResolveResourceDefinition(resource.GetClrType()); resourceDefinition.OnSerialize((dynamic)resource); } diff --git a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs index fa3e571c7c..c276c073cd 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using System.Reflection; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Internal; using Microsoft.Extensions.DependencyInjection; @@ -8,6 +9,8 @@ namespace JsonApiDotNetCore.Resources; /// internal sealed class ResourceFactory : IResourceFactory { + private static readonly TypeLocator TypeLocator = new(); + private readonly IServiceProvider _serviceProvider; public ResourceFactory(IServiceProvider serviceProvider) @@ -22,9 +25,35 @@ public IIdentifiable CreateInstance(Type resourceClrType) { ArgumentGuard.NotNull(resourceClrType, nameof(resourceClrType)); + if (!resourceClrType.IsAssignableTo(typeof(IIdentifiable))) + { + throw new InvalidOperationException($"Resource type '{resourceClrType}' does not implement IIdentifiable."); + } + + if (resourceClrType.IsAbstract) + { + return CreateWrapperForAbstractType(resourceClrType); + } + return InnerCreateInstance(resourceClrType, _serviceProvider); } + private static IIdentifiable CreateWrapperForAbstractType(Type resourceClrType) + { + ResourceDescriptor? descriptor = TypeLocator.ResolveResourceDescriptor(resourceClrType); + + if (descriptor == null) + { + throw new InvalidOperationException($"Resource type '{resourceClrType}' implements 'IIdentifiable', but not 'IIdentifiable'."); + } + + Type wrapperClrType = typeof(AbstractResourceWrapper<>).MakeGenericType(descriptor.IdClrType); + ConstructorInfo constructor = wrapperClrType.GetConstructors().Single(); + + object resource = constructor.Invoke(ArrayFactory.Create(resourceClrType)); + return (IIdentifiable)resource; + } + /// public TResource CreateInstance() where TResource : IIdentifiable diff --git a/src/JsonApiDotNetCore/Resources/SortExpressionLambdaConverter.cs b/src/JsonApiDotNetCore/Resources/SortExpressionLambdaConverter.cs new file mode 100644 index 0000000000..a2371419b6 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/SortExpressionLambdaConverter.cs @@ -0,0 +1,170 @@ +using System.Collections.Immutable; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Resources; + +internal sealed class SortExpressionLambdaConverter +{ + private readonly IResourceGraph _resourceGraph; + private readonly IList _fields = new List(); + + public SortExpressionLambdaConverter(IResourceGraph resourceGraph) + { + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + + _resourceGraph = resourceGraph; + } + + public SortElementExpression FromLambda(Expression> keySelector, ListSortDirection sortDirection) + { + ArgumentGuard.NotNull(keySelector, nameof(keySelector)); + + _fields.Clear(); + + Expression lambdaBodyExpression = SkipConvert(keySelector.Body); + (Expression? expression, bool isCount) = TryReadCount(lambdaBodyExpression); + + if (expression != null) + { + expression = SkipConvert(expression); + expression = isCount ? ReadToManyRelationship(expression) : ReadAttribute(expression); + + while (expression != null) + { + expression = SkipConvert(expression); + + if (IsLambdaParameter(expression, keySelector.Parameters[0])) + { + return ToSortElement(isCount, sortDirection); + } + + expression = ReadToOneRelationship(expression); + } + } + + throw new InvalidOperationException($"Unsupported expression body '{lambdaBodyExpression}'."); + } + + private static Expression SkipConvert(Expression expression) + { + Expression inner = expression; + + while (true) + { + if (inner is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.TypeAs } unary) + { + inner = unary.Operand; + } + else + { + return inner; + } + } + } + + private static (Expression? innerExpression, bool isCount) TryReadCount(Expression expression) + { + if (expression is MethodCallExpression methodCallExpression && methodCallExpression.Method.Name == "Count") + { + if (methodCallExpression.Arguments.Count <= 1) + { + return (methodCallExpression.Arguments[0], true); + } + + throw new InvalidOperationException("Count method that takes a predicate is not supported."); + } + + if (expression is MemberExpression memberExpression) + { + if (memberExpression.Member.MemberType == MemberTypes.Property && memberExpression.Member.Name is "Count" or "Length") + { + if (memberExpression.Member.GetCustomAttribute() == null) + { + return (memberExpression.Expression, true); + } + } + + return (memberExpression, false); + } + + return (null, false); + } + + private Expression? ReadToManyRelationship(Expression expression) + { + if (expression is MemberExpression memberExpression) + { + ResourceType resourceType = _resourceGraph.GetResourceType(memberExpression.Member.DeclaringType!); + RelationshipAttribute? relationship = resourceType.FindRelationshipByPropertyName(memberExpression.Member.Name); + + if (relationship is HasManyAttribute) + { + _fields.Insert(0, relationship); + return memberExpression.Expression; + } + } + + throw new InvalidOperationException($"Expected property for JSON:API to-many relationship, but found '{expression}'."); + } + + private Expression? ReadAttribute(Expression expression) + { + if (expression is MemberExpression memberExpression) + { + ResourceType resourceType = memberExpression.Member.Name == nameof(Identifiable.Id) && memberExpression.Expression != null + ? _resourceGraph.GetResourceType(memberExpression.Expression.Type) + : _resourceGraph.GetResourceType(memberExpression.Member.DeclaringType!); + + AttrAttribute? attribute = resourceType.FindAttributeByPropertyName(memberExpression.Member.Name); + + if (attribute != null) + { + _fields.Insert(0, attribute); + return memberExpression.Expression; + } + } + + throw new InvalidOperationException($"Expected property for JSON:API attribute, but found '{expression}'."); + } + + private Expression? ReadToOneRelationship(Expression expression) + { + if (expression is MemberExpression memberExpression) + { + ResourceType resourceType = _resourceGraph.GetResourceType(memberExpression.Member.DeclaringType!); + RelationshipAttribute? relationship = resourceType.FindRelationshipByPropertyName(memberExpression.Member.Name); + + if (relationship is HasOneAttribute) + { + _fields.Insert(0, relationship); + return memberExpression.Expression; + } + } + + throw new InvalidOperationException($"Expected property for JSON:API to-one relationship, but found '{expression}'."); + } + + private static bool IsLambdaParameter(Expression expression, ParameterExpression lambdaParameter) + { + return expression is ParameterExpression parameterExpression && parameterExpression.Name == lambdaParameter.Name; + } + + private SortElementExpression ToSortElement(bool isCount, ListSortDirection sortDirection) + { + var chain = new ResourceFieldChainExpression(_fields.ToImmutableArray()); + bool isAscending = sortDirection == ListSortDirection.Ascending; + + if (isCount) + { + var countExpression = new CountExpression(chain); + return new SortElementExpression(countExpression, isAscending); + } + + return new SortElementExpression(chain, isAscending); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInOperationsRequestAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInOperationsRequestAdapter.cs index 6d26125666..8a50db9fec 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInOperationsRequestAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInOperationsRequestAdapter.cs @@ -21,7 +21,7 @@ public DocumentInOperationsRequestAdapter(IJsonApiOptions options, IAtomicOperat } /// - public IList Convert(Document document, RequestAdapterState state) + public IReadOnlyList Convert(Document document, RequestAdapterState state) { ArgumentGuard.NotNull(state, nameof(state)); AssertHasOperations(document.Operations, state); @@ -50,7 +50,7 @@ private void AssertMaxOperationsNotExceeded(ICollection } } - private IList ConvertOperations(IEnumerable atomicOperationObjects, RequestAdapterState state) + private IReadOnlyList ConvertOperations(IEnumerable atomicOperationObjects, RequestAdapterState state) { var operations = new List(); int operationIndex = 0; diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentInOperationsRequestAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentInOperationsRequestAdapter.cs index 228abbf3ce..b4e929cba3 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentInOperationsRequestAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentInOperationsRequestAdapter.cs @@ -11,5 +11,5 @@ public interface IDocumentInOperationsRequestAdapter /// /// Validates and converts the specified . /// - IList Convert(Document document, RequestAdapterState state); + IReadOnlyList Convert(Document document, RequestAdapterState state); } diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs index f453b8dd21..d163eb56d1 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs @@ -46,6 +46,12 @@ private ResourceType ResolveType(IResourceIdentity identity, ResourceIdentityReq ResourceType? resourceType = _resourceGraph.FindResourceType(identity.Type); AssertIsKnownResourceType(resourceType, identity.Type, state); + + if (state.Request.WriteOperation is WriteOperationKind.CreateResource or WriteOperationKind.UpdateResource) + { + AssertIsNotAbstractType(resourceType, identity.Type, state); + } + AssertIsCompatibleResourceType(resourceType, requirements.ResourceType, requirements.RelationshipName, state); return resourceType; @@ -67,13 +73,21 @@ private static void AssertIsKnownResourceType([NotNull] ResourceType? resourceTy } } + private static void AssertIsNotAbstractType(ResourceType resourceType, string typeName, RequestAdapterState state) + { + if (resourceType.ClrType.IsAbstract) + { + throw new ModelConversionException(state.Position, "Abstract resource type found.", $"Resource type '{typeName}' is abstract."); + } + } + private static void AssertIsCompatibleResourceType(ResourceType actual, ResourceType? expected, string? relationshipName, RequestAdapterState state) { if (expected != null && !expected.ClrType.IsAssignableFrom(actual.ClrType)) { string message = relationshipName != null - ? $"Type '{actual.PublicName}' is incompatible with type '{expected.PublicName}' of relationship '{relationshipName}'." - : $"Type '{actual.PublicName}' is incompatible with type '{expected.PublicName}'."; + ? $"Type '{actual.PublicName}' is not convertible to type '{expected.PublicName}' of relationship '{relationshipName}'." + : $"Type '{actual.PublicName}' is not convertible to type '{expected.PublicName}'."; throw new ModelConversionException(state.Position, "Incompatible resource type found.", message, HttpStatusCode.Conflict); } diff --git a/src/JsonApiDotNetCore/Serialization/Response/EmptyResponseMeta.cs b/src/JsonApiDotNetCore/Serialization/Response/EmptyResponseMeta.cs index 77b2058796..dfdb20bde1 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/EmptyResponseMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/EmptyResponseMeta.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCore.Serialization.Response; public sealed class EmptyResponseMeta : IResponseMeta { /// - public IReadOnlyDictionary? GetMeta() + public IDictionary? GetMeta() { return null; } diff --git a/src/JsonApiDotNetCore/Serialization/Response/IMetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/IMetaBuilder.cs index e804179505..1c3916c404 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/IMetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/IMetaBuilder.cs @@ -12,10 +12,12 @@ 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. /// - void Add(IReadOnlyDictionary values); + void Add(IDictionary values); /// /// Builds the top-level meta data object. /// +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection IDictionary? Build(); +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection } diff --git a/src/JsonApiDotNetCore/Serialization/Response/IResponseMeta.cs b/src/JsonApiDotNetCore/Serialization/Response/IResponseMeta.cs index f3035cfa4c..2e17ec9bdb 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/IResponseMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/IResponseMeta.cs @@ -12,5 +12,7 @@ public interface IResponseMeta /// /// Gets the global top-level JSON:API meta information to add to the response. /// - IReadOnlyDictionary? GetMeta(); +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection + IDictionary? GetMeta(); +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection } diff --git a/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs index 38e63ea9cb..0bad02066b 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs @@ -312,6 +312,13 @@ private bool ShouldIncludeResourceLink(LinkTypes linkType, ResourceType resource protected virtual string? RenderLinkForAction(string? controllerName, string actionName, IDictionary routeValues) { + if (controllerName == null) + { + // When passing null to LinkGenerator, it uses the controller for the current endpoint. This is incorrect for + // included resources of a different resource type: it should hide its links when there's no controller for them. + return null; + } + return _options.UseRelativeLinks ? _linkGenerator.GetPathByAction(HttpContext, actionName, controllerName, routeValues) : _linkGenerator.GetUriByAction(HttpContext, actionName, controllerName, routeValues); diff --git a/src/JsonApiDotNetCore/Serialization/Response/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/MetaBuilder.cs index d250501d60..91ec62387c 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/MetaBuilder.cs @@ -26,7 +26,7 @@ public MetaBuilder(IPaginationContext paginationContext, IJsonApiOptions options } /// - public void Add(IReadOnlyDictionary values) + public void Add(IDictionary values) { ArgumentGuard.NotNull(values, nameof(values)); @@ -43,7 +43,7 @@ public void Add(IReadOnlyDictionary values) _meta.Add(key, _paginationContext.TotalResourceCount); } - IReadOnlyDictionary? extraMeta = _responseMeta.GetMeta(); + IDictionary? extraMeta = _responseMeta.GetMeta(); if (extraMeta != null) { diff --git a/src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs b/src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs index df75fcaa66..aed19d9097 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs @@ -88,7 +88,7 @@ public void AttachRelationshipChild(RelationshipAttribute relationship, Resource /// /// Recursively walks the tree and returns the set of unique nodes. Uses relationship declaration order. /// - public ISet GetUniqueNodes() + public IReadOnlySet GetUniqueNodes() { AssertIsTreeRoot(); @@ -148,7 +148,7 @@ private static void VisitRelationshipChildInSubtree(HashSet? GetRightNodesInRelationship(RelationshipAttribute relationship) + public IReadOnlySet? GetRightNodesInRelationship(RelationshipAttribute relationship) { return _childrenByRelationship != null && _childrenByRelationship.TryGetValue(relationship, out HashSet? rightNodes) ? rightNodes @@ -158,7 +158,7 @@ private static void VisitRelationshipChildInSubtree(HashSet /// Provides the value for 'data' in the response body. Uses relationship declaration order. /// - public IList GetResponseData() + public IReadOnlyList GetResponseData() { AssertIsTreeRoot(); @@ -168,7 +168,9 @@ public IList GetResponseData() /// /// Provides the value for 'included' in the response body. Uses relationship declaration order. /// +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection public IList GetResponseIncluded() +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection { AssertIsTreeRoot(); @@ -251,19 +253,19 @@ private ResourceObjectComparer() { } - public bool Equals(ResourceObject? x, ResourceObject? y) + public bool Equals(ResourceObject? left, ResourceObject? right) { - if (ReferenceEquals(x, y)) + if (ReferenceEquals(left, right)) { return true; } - if (x is null || y is null || x.GetType() != y.GetType()) + if (left is null || right is null || left.GetType() != right.GetType()) { return false; } - return x.Type == y.Type && x.Id == y.Id && x.Lid == y.Lid; + return left.Type == right.Type && left.Id == right.Id && left.Lid == right.Lid; } public int GetHashCode(ResourceObject obj) diff --git a/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs index d881668c9f..223166e59e 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs @@ -3,6 +3,7 @@ using JetBrains.Annotations; using JsonApiDotNetCore.AtomicOperations; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Queries.Internal; @@ -79,7 +80,7 @@ public Document Convert(object? model) PopulateRelationshipsInTree(rootNode, _request.Kind); - IEnumerable resourceObjects = rootNode.GetResponseData(); + IReadOnlyList resourceObjects = rootNode.GetResponseData(); document.Data = new SingleOrManyData(resourceObjects); } else if (model is IIdentifiable resource) @@ -172,8 +173,11 @@ private ResourceObjectTreeNode GetOrCreateTreeNode(IIdentifiable resource, Resou { if (!_resourceToTreeNodeCache.TryGetValue(resource, out ResourceObjectTreeNode? treeNode)) { - ResourceObject resourceObject = ConvertResource(resource, resourceType, kind); - treeNode = new ResourceObjectTreeNode(resource, resourceType, resourceObject); + // In case of resource inheritance, prefer the derived resource type over the base type. + ResourceType effectiveResourceType = GetEffectiveResourceType(resource, resourceType); + + ResourceObject resourceObject = ConvertResource(resource, effectiveResourceType, kind); + treeNode = new ResourceObjectTreeNode(resource, effectiveResourceType, resourceObject); _resourceToTreeNodeCache.Add(resource, treeNode); } @@ -181,6 +185,25 @@ private ResourceObjectTreeNode GetOrCreateTreeNode(IIdentifiable resource, Resou return treeNode; } + private static ResourceType GetEffectiveResourceType(IIdentifiable resource, ResourceType declaredType) + { + Type runtimeResourceType = resource.GetClrType(); + + if (declaredType.ClrType == runtimeResourceType) + { + return declaredType; + } + + ResourceType? derivedType = declaredType.GetAllConcreteDerivedTypes().FirstOrDefault(type => type.ClrType == runtimeResourceType); + + if (derivedType == null) + { + throw new InvalidConfigurationException($"Type '{runtimeResourceType}' does not exist in the resource graph."); + } + + return derivedType; + } + protected virtual ResourceObject ConvertResource(IIdentifiable resource, ResourceType resourceType, EndpointKind kind) { bool isRelationship = kind == EndpointKind.Relationship; @@ -208,7 +231,9 @@ protected virtual ResourceObject ConvertResource(IIdentifiable resource, Resourc return resourceObject; } +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection protected virtual IDictionary? ConvertAttributes(IIdentifiable resource, ResourceType resourceType, +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection IImmutableSet fieldSet) { var attrMap = new Dictionary(resourceType.Attributes.Count); @@ -251,14 +276,25 @@ private void TraverseRelationships(IIdentifiable leftResource, ResourceObjectTre private void TraverseRelationship(RelationshipAttribute relationship, IIdentifiable leftResource, ResourceObjectTreeNode leftTreeNode, IncludeElementExpression includeElement, EndpointKind kind) { - object? rightValue = relationship.GetValue(leftResource); - ICollection rightResources = CollectionConverter.ExtractResources(rightValue); + if (!relationship.LeftType.ClrType.IsAssignableFrom(leftTreeNode.ResourceType.ClrType)) + { + // Skipping over relationship that is declared on another derived type. + return; + } + + // In case of resource inheritance, prefer the relationship on derived type over the one on base type. + RelationshipAttribute effectiveRelationship = !leftTreeNode.ResourceType.Equals(relationship.LeftType) + ? leftTreeNode.ResourceType.GetRelationshipByPropertyName(relationship.Property.Name) + : relationship; + + object? rightValue = effectiveRelationship.GetValue(leftResource); + IReadOnlyCollection rightResources = CollectionConverter.ExtractResources(rightValue); - leftTreeNode.EnsureHasRelationship(relationship); + leftTreeNode.EnsureHasRelationship(effectiveRelationship); foreach (IIdentifiable rightResource in rightResources) { - TraverseResource(rightResource, relationship.RightType, kind, includeElement.Children, leftTreeNode, relationship); + TraverseResource(rightResource, effectiveRelationship.RightType, kind, includeElement.Children, leftTreeNode, effectiveRelationship); } } @@ -306,7 +342,7 @@ private void PopulateRelationshipInResourceObject(ResourceObjectTreeNode treeNod private static SingleOrManyData GetRelationshipData(ResourceObjectTreeNode treeNode, RelationshipAttribute relationship) { - ISet? rightNodes = treeNode.GetRightNodesInRelationship(relationship); + IReadOnlySet? rightNodes = treeNode.GetRightNodesInRelationship(relationship); if (rightNodes != null) { diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index 87046783c5..0cca8b92b9 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -196,14 +196,17 @@ private async Task RetrieveResourceCountForNonPrimaryEndpointAsync(TId id, HasMa }); ArgumentGuard.NotNull(resource, nameof(resource)); - AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType); using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Create resource"); TResource resourceFromRequest = resource; _resourceChangeTracker.SetRequestAttributeValues(resourceFromRequest); - TResource resourceForDatabase = await _repositoryAccessor.GetForCreateAsync(resource.Id, cancellationToken); + await AccurizeResourceTypesInHierarchyToAssignInRelationshipsAsync(resourceFromRequest, cancellationToken); + + Type resourceClrType = resourceFromRequest.GetClrType(); + TResource resourceForDatabase = await _repositoryAccessor.GetForCreateAsync(resourceClrType, resourceFromRequest.Id, cancellationToken); + AccurizeJsonApiRequest(resourceForDatabase); _resourceChangeTracker.SetInitiallyStoredAttributeValues(resourceForDatabase); @@ -246,19 +249,44 @@ protected virtual async Task InitializeResourceAsync(TResource resourceForDataba await _resourceDefinitionAccessor.OnPrepareWriteAsync(resourceForDatabase, WriteOperationKind.CreateResource, cancellationToken); } + private async Task AccurizeResourceTypesInHierarchyToAssignInRelationshipsAsync(TResource primaryResource, CancellationToken cancellationToken) + { + await ValidateResourcesToAssignInRelationshipsExistWithRefreshAsync(primaryResource, true, cancellationToken); + } + protected async Task AssertResourcesToAssignInRelationshipsExistAsync(TResource primaryResource, CancellationToken cancellationToken) + { + await ValidateResourcesToAssignInRelationshipsExistWithRefreshAsync(primaryResource, false, cancellationToken); + } + + private async Task ValidateResourcesToAssignInRelationshipsExistWithRefreshAsync(TResource primaryResource, bool onlyIfTypeHierarchy, + CancellationToken cancellationToken) { var missingResources = new List(); foreach ((QueryLayer queryLayer, RelationshipAttribute relationship) in _queryLayerComposer.ComposeForGetTargetedSecondaryResourceIds(primaryResource)) { - object? rightValue = relationship.GetValue(primaryResource); - ICollection rightResourceIds = _collectionConverter.ExtractResources(rightValue); + if (!onlyIfTypeHierarchy || relationship.RightType.IsPartOfTypeHierarchy()) + { + object? rightValue = relationship.GetValue(primaryResource); + HashSet rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); + + if (rightResourceIds.Any()) + { + IAsyncEnumerable missingResourcesInRelationship = + GetMissingRightResourcesAsync(queryLayer, relationship, rightResourceIds, cancellationToken); - IAsyncEnumerable missingResourcesInRelationship = - GetMissingRightResourcesAsync(queryLayer, relationship, rightResourceIds, cancellationToken); + await missingResources.AddRangeAsync(missingResourcesInRelationship, cancellationToken); - await missingResources.AddRangeAsync(missingResourcesInRelationship, cancellationToken); + // Some of the right-side resources from request may be typed as base types, but stored as derived types. + // Now that we've fetched them, update the request types so that resource definitions observe the actually stored types. + object? newRightValue = relationship is HasOneAttribute + ? rightResourceIds.FirstOrDefault() + : _collectionConverter.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType); + + relationship.SetValue(primaryResource, newRightValue); + } + } } if (missingResources.Any()) @@ -268,20 +296,35 @@ protected async Task AssertResourcesToAssignInRelationshipsExistAsync(TResource } private async IAsyncEnumerable GetMissingRightResourcesAsync(QueryLayer existingRightResourceIdsQueryLayer, - RelationshipAttribute relationship, ICollection rightResourceIds, [EnumeratorCancellation] CancellationToken cancellationToken) + RelationshipAttribute relationship, ISet rightResourceIds, [EnumeratorCancellation] CancellationToken cancellationToken) { IReadOnlyCollection existingResources = await _repositoryAccessor.GetAsync(existingRightResourceIdsQueryLayer.ResourceType, existingRightResourceIdsQueryLayer, cancellationToken); - string[] existingResourceIds = existingResources.Select(resource => resource.StringId!).ToArray(); - - foreach (IIdentifiable rightResourceId in rightResourceIds) + foreach (IIdentifiable rightResourceId in rightResourceIds.ToArray()) { - if (!existingResourceIds.Contains(rightResourceId.StringId)) + Type rightResourceClrType = rightResourceId.GetClrType(); + IIdentifiable? existingResourceId = existingResources.FirstOrDefault(resource => resource.StringId == rightResourceId.StringId); + + if (existingResourceId != null) { - yield return new MissingResourceInRelationship(relationship.PublicName, existingRightResourceIdsQueryLayer.ResourceType.PublicName, - rightResourceId.StringId!); + Type existingResourceClrType = existingResourceId.GetClrType(); + + if (rightResourceClrType.IsAssignableFrom(existingResourceClrType)) + { + if (rightResourceClrType != existingResourceClrType) + { + // PERF: As a side effect, we replace the resource base type from request with the derived type that is stored. + rightResourceIds.Remove(rightResourceId); + rightResourceIds.Add(existingResourceId); + } + + continue; + } } + + ResourceType requestResourceType = relationship.RightType.GetTypeOrDerived(rightResourceClrType); + yield return new MissingResourceInRelationship(relationship.PublicName, requestResourceType.PublicName, rightResourceId.StringId!); } } @@ -292,6 +335,7 @@ public virtual async Task AddToToManyRelationshipAsync(TId leftId, string relati _traceWriter.LogMethodStart(new { leftId, + relationshipName, rightResourceIds }); @@ -302,36 +346,56 @@ public virtual async Task AddToToManyRelationshipAsync(TId leftId, string relati AssertHasRelationship(_request.Relationship, relationshipName); + TResource? resourceFromDatabase = null; + if (rightResourceIds.Any() && _request.Relationship is HasManyAttribute { IsManyToMany: true } manyToManyRelationship) { // In the case of a many-to-many relationship, creating a duplicate entry in the join table results in a // unique constraint violation. We avoid that by excluding already-existing entries from the set in advance. - await RemoveExistingIdsFromRelationshipRightSideAsync(manyToManyRelationship, leftId, rightResourceIds, cancellationToken); + resourceFromDatabase = await RemoveExistingIdsFromRelationshipRightSideAsync(manyToManyRelationship, leftId, rightResourceIds, cancellationToken); + } + + if (_request.Relationship.LeftType.IsPartOfTypeHierarchy()) + { + // The left resource may be stored as a derived type. We fetch it, so we'll know the stored type, which + // enables to invoke IResourceDefinition with TResource being the stored resource type. + resourceFromDatabase ??= await GetPrimaryResourceForUpdateAsync(leftId, cancellationToken); + AccurizeJsonApiRequest(resourceFromDatabase); + } + + ISet effectiveRightResourceIds = rightResourceIds; + + if (_request.Relationship.RightType.IsPartOfTypeHierarchy()) + { + // Some of the incoming right-side resources may be stored as a derived type. We fetch them, so we'll know + // the stored types, which enables to invoke resource definitions with the stored right-side resources types. + object? rightValue = await AssertRightResourcesExistAsync(rightResourceIds, cancellationToken); + effectiveRightResourceIds = ((IEnumerable)rightValue!).ToHashSet(IdentifiableComparer.Instance); } try { - await _repositoryAccessor.AddToToManyRelationshipAsync(leftId, rightResourceIds, cancellationToken); + await _repositoryAccessor.AddToToManyRelationshipAsync(resourceFromDatabase, leftId, effectiveRightResourceIds, cancellationToken); } catch (DataStoreUpdateException) { await GetPrimaryResourceByIdAsync(leftId, TopFieldSelection.OnlyIdAttribute, cancellationToken); - await AssertRightResourcesExistAsync(rightResourceIds, cancellationToken); + await AssertRightResourcesExistAsync(effectiveRightResourceIds, cancellationToken); throw; } } - private async Task RemoveExistingIdsFromRelationshipRightSideAsync(HasManyAttribute hasManyRelationship, TId leftId, ISet rightResourceIds, - CancellationToken cancellationToken) + private async Task RemoveExistingIdsFromRelationshipRightSideAsync(HasManyAttribute hasManyRelationship, TId leftId, + ISet rightResourceIds, CancellationToken cancellationToken) { - AssertRelationshipInJsonApiRequestIsNotNull(_request.Relationship); - TResource leftResource = await GetForHasManyUpdateAsync(hasManyRelationship, leftId, rightResourceIds, cancellationToken); - object? rightValue = _request.Relationship.GetValue(leftResource); - ICollection existingRightResourceIds = _collectionConverter.ExtractResources(rightValue); + object? rightValue = hasManyRelationship.GetValue(leftResource); + IReadOnlyCollection existingRightResourceIds = _collectionConverter.ExtractResources(rightValue); rightResourceIds.ExceptWith(existingRightResourceIds); + + return leftResource; } private async Task GetForHasManyUpdateAsync(HasManyAttribute hasManyRelationship, TId leftId, ISet rightResourceIds, @@ -344,11 +408,12 @@ private async Task GetForHasManyUpdateAsync(HasManyAttribute hasManyR return leftResource; } - protected async Task AssertRightResourcesExistAsync(object? rightValue, CancellationToken cancellationToken) + protected async Task AssertRightResourcesExistAsync(object? rightValue, CancellationToken cancellationToken) { AssertRelationshipInJsonApiRequestIsNotNull(_request.Relationship); - ICollection rightResourceIds = _collectionConverter.ExtractResources(rightValue); + HashSet rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); + object? newRightValue = rightValue; if (rightResourceIds.Any()) { @@ -357,11 +422,19 @@ protected async Task AssertRightResourcesExistAsync(object? rightValue, Cancella List missingResources = await GetMissingRightResourcesAsync(queryLayer, _request.Relationship, rightResourceIds, cancellationToken).ToListAsync(cancellationToken); + // Some of the right-side resources from request may be typed as base types, but stored as derived types. + // Now that we've fetched them, update the request types so that resource definitions observe the actually stored types. + newRightValue = _request.Relationship is HasOneAttribute + ? rightResourceIds.FirstOrDefault() + : _collectionConverter.CopyToTypedCollection(rightResourceIds, _request.Relationship.Property.PropertyType); + if (missingResources.Any()) { throw new ResourcesInRelationshipsNotFoundException(missingResources); } } + + return newRightValue; } /// @@ -380,7 +453,10 @@ protected async Task AssertRightResourcesExistAsync(object? rightValue, Cancella TResource resourceFromRequest = resource; _resourceChangeTracker.SetRequestAttributeValues(resourceFromRequest); + await AccurizeResourceTypesInHierarchyToAssignInRelationshipsAsync(resourceFromRequest, cancellationToken); + TResource resourceFromDatabase = await GetPrimaryResourceForUpdateAsync(id, cancellationToken); + AccurizeJsonApiRequest(resourceFromDatabase); _resourceChangeTracker.SetInitiallyStoredAttributeValues(resourceFromDatabase); @@ -420,17 +496,24 @@ public virtual async Task SetRelationshipAsync(TId leftId, string relationshipNa AssertHasRelationship(_request.Relationship, relationshipName); + object? effectiveRightValue = _request.Relationship.RightType.IsPartOfTypeHierarchy() + // Some of the incoming right-side resources may be stored as a derived type. We fetch them, so we'll know + // the stored types, which enables to invoke resource definitions with the stored right-side resources types. + ? await AssertRightResourcesExistAsync(rightValue, cancellationToken) + : rightValue; + TResource resourceFromDatabase = await GetPrimaryResourceForUpdateAsync(leftId, cancellationToken); + AccurizeJsonApiRequest(resourceFromDatabase); await _resourceDefinitionAccessor.OnPrepareWriteAsync(resourceFromDatabase, WriteOperationKind.SetRelationship, cancellationToken); try { - await _repositoryAccessor.SetRelationshipAsync(resourceFromDatabase, rightValue, cancellationToken); + await _repositoryAccessor.SetRelationshipAsync(resourceFromDatabase, effectiveRightValue, cancellationToken); } catch (DataStoreUpdateException) { - await AssertRightResourcesExistAsync(rightValue, cancellationToken); + await AssertRightResourcesExistAsync(effectiveRightValue, cancellationToken); throw; } } @@ -445,9 +528,21 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Delete resource"); + AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType); + + TResource? resourceFromDatabase = null; + + if (_request.PrimaryResourceType.IsPartOfTypeHierarchy()) + { + // The resource to delete may be stored as a derived type. We fetch it, so we'll know the stored type, which + // enables to invoke IResourceDefinition with TResource being the stored resource type. + resourceFromDatabase = await GetPrimaryResourceForUpdateAsync(id, cancellationToken); + AccurizeJsonApiRequest(resourceFromDatabase); + } + try { - await _repositoryAccessor.DeleteAsync(id, cancellationToken); + await _repositoryAccessor.DeleteAsync(resourceFromDatabase, id, cancellationToken); } catch (DataStoreUpdateException) { @@ -476,10 +571,14 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TId leftId, string r var hasManyRelationship = (HasManyAttribute)_request.Relationship; TResource resourceFromDatabase = await GetForHasManyUpdateAsync(hasManyRelationship, leftId, rightResourceIds, cancellationToken); + AccurizeJsonApiRequest(resourceFromDatabase); + + object? rightValue = await AssertRightResourcesExistAsync(rightResourceIds, cancellationToken); + ISet effectiveRightResourceIds = ((IEnumerable)rightValue!).ToHashSet(IdentifiableComparer.Instance); - await AssertRightResourcesExistAsync(rightResourceIds, cancellationToken); + await _resourceDefinitionAccessor.OnPrepareWriteAsync(resourceFromDatabase, WriteOperationKind.SetRelationship, cancellationToken); - await _repositoryAccessor.RemoveFromToManyRelationshipAsync(resourceFromDatabase, rightResourceIds, cancellationToken); + await _repositoryAccessor.RemoveFromToManyRelationshipAsync(resourceFromDatabase, effectiveRightResourceIds, cancellationToken); } protected async Task GetPrimaryResourceByIdAsync(TId id, TopFieldSelection fieldSelection, CancellationToken cancellationToken) @@ -511,6 +610,33 @@ protected async Task GetPrimaryResourceForUpdateAsync(TId id, Cancell return resource; } + private void AccurizeJsonApiRequest(TResource resourceFromDatabase) + { + // When using resource inheritance, the stored left-side resource may be more derived than what this endpoint assumes. + // In that case, we promote data in IJsonApiRequest to better represent what is going on. + + Type storedType = resourceFromDatabase.GetClrType(); + + if (storedType != typeof(TResource)) + { + AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType); + ResourceType? derivedType = _request.PrimaryResourceType.GetAllConcreteDerivedTypes().FirstOrDefault(type => type.ClrType == storedType); + + if (derivedType == null) + { + throw new InvalidConfigurationException($"Type '{storedType}' does not exist in the resource graph."); + } + + var request = (JsonApiRequest)_request; + request.PrimaryResourceType = derivedType; + + if (request.Relationship != null) + { + request.Relationship = derivedType.GetRelationshipByPublicName(request.Relationship.PublicName); + } + } + } + [AssertionMethod] private void AssertPrimaryResourceExists([SysNotNull] TResource? resource) { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs index 5053d17e09..2af39b3d26 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs @@ -45,7 +45,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(broadcast.StringId); @@ -71,7 +71,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(broadcast.StringId); @@ -98,7 +98,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(broadcasts[1].StringId); @@ -125,7 +125,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(2); responseDocument.Data.ManyValue[0].Id.Should().Be(broadcasts[0].StringId); @@ -155,7 +155,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(station.StringId); @@ -185,7 +185,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(station.StringId); @@ -216,7 +216,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(comment.AppliesTo.StringId); @@ -243,7 +243,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); @@ -270,7 +270,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); DateTimeOffset archivedAt0 = station.Broadcasts.ElementAt(0).ArchivedAt!.Value; @@ -303,7 +303,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(network.Stations.ElementAt(0).StringId); @@ -334,7 +334,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); DateTimeOffset archivedAt0 = network.Stations.ElementAt(0).Broadcasts.ElementAt(0).ArchivedAt!.Value; @@ -368,7 +368,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); @@ -394,7 +394,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(2); responseDocument.Data.ManyValue[0].Id.Should().Be(station.Broadcasts.ElementAt(0).StringId); @@ -426,7 +426,7 @@ public async Task Can_create_unarchived_resource() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("title").With(value => value.Should().Be(newBroadcast.Title)); @@ -460,7 +460,7 @@ public async Task Cannot_create_archived_resource() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.ShouldHaveCount(1); @@ -504,7 +504,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -547,7 +547,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -592,7 +592,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.ShouldHaveCount(1); @@ -620,7 +620,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -651,7 +651,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs index 6908a2b296..e56e9119bf 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs @@ -63,7 +63,7 @@ public async Task Can_create_resources_for_matching_resource_type() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); } @@ -96,7 +96,7 @@ public async Task Cannot_create_resource_for_mismatching_resource_type() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -145,7 +145,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -201,7 +201,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index f37d53b275..ffae461fb0 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -63,7 +63,7 @@ public async Task Can_create_resource() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -126,7 +126,7 @@ public async Task Can_create_resources() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(elementCount); @@ -199,7 +199,7 @@ public async Task Can_create_resource_without_attributes_or_relationships() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -254,7 +254,7 @@ public async Task Cannot_create_resource_with_unknown_attribute() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -302,7 +302,7 @@ public async Task Can_create_resource_with_unknown_attribute() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -359,7 +359,7 @@ public async Task Cannot_create_resource_with_unknown_relationship() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -417,7 +417,7 @@ public async Task Can_create_resource_with_unknown_relationship() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -471,7 +471,7 @@ public async Task Cannot_create_resource_with_client_generated_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.ShouldHaveCount(1); @@ -506,7 +506,7 @@ public async Task Cannot_create_resource_for_href_element() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -544,7 +544,7 @@ public async Task Cannot_create_resource_for_ref_element() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -578,7 +578,7 @@ public async Task Cannot_create_resource_for_missing_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -613,7 +613,7 @@ public async Task Cannot_create_resource_for_null_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -660,7 +660,7 @@ public async Task Cannot_create_resource_for_array_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -700,7 +700,7 @@ public async Task Cannot_create_resource_for_missing_type() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -738,7 +738,7 @@ public async Task Cannot_create_resource_for_unknown_type() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -780,7 +780,7 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -825,7 +825,7 @@ public async Task Cannot_create_resource_with_readonly_attribute() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -867,7 +867,7 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -951,7 +951,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs index ef64ebc64a..62907c047a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs @@ -67,7 +67,7 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); string isoCode = $"{newLanguage.IsoCode}{ImplicitlyChangingTextLanguageDefinition.Suffix}"; @@ -124,7 +124,7 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_ (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -179,7 +179,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.ShouldHaveCount(1); @@ -223,7 +223,7 @@ public async Task Cannot_create_resource_for_incompatible_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -263,7 +263,7 @@ public async Task Cannot_create_resource_for_ID_and_local_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index 465f39cee1..be9700c3d6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -81,7 +81,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -166,7 +166,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -228,7 +228,7 @@ public async Task Cannot_create_for_missing_relationship_type() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -280,7 +280,7 @@ public async Task Cannot_create_for_unknown_relationship_type() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -331,7 +331,7 @@ public async Task Cannot_create_for_missing_relationship_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -397,7 +397,7 @@ public async Task Cannot_create_for_unknown_relationship_IDs() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(2); @@ -457,14 +457,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.ShouldHaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); - error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); + error.Detail.Should().Be("Type 'playlists' is not convertible to type 'performers' of relationship 'performers'."); error.Source.ShouldNotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty()); @@ -527,7 +527,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -580,7 +580,7 @@ public async Task Cannot_create_with_missing_data_in_OneToMany_relationship() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -625,7 +625,7 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -672,7 +672,7 @@ public async Task Cannot_create_with_object_data_in_ManyToMany_relationship() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index 64fc6e66d3..e7ba0d5288 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -75,7 +75,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -146,7 +146,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -223,7 +223,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(elementCount); @@ -294,7 +294,7 @@ public async Task Cannot_create_for_null_relationship() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -338,7 +338,7 @@ public async Task Cannot_create_for_missing_data_in_relationship() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -390,7 +390,7 @@ public async Task Cannot_create_for_array_data_in_relationship() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -438,7 +438,7 @@ public async Task Cannot_create_for_missing_relationship_type() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -487,7 +487,7 @@ public async Task Cannot_create_for_unknown_relationship_type() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -535,7 +535,7 @@ public async Task Cannot_create_for_missing_relationship_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -592,7 +592,7 @@ public async Task Cannot_create_with_unknown_relationship_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -641,14 +641,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.ShouldHaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); - error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'."); + error.Detail.Should().Be("Type 'playlists' is not convertible to type 'lyrics' of relationship 'lyric'."); error.Source.ShouldNotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty()); @@ -713,7 +713,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBodyText); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index 45dbb11f5e..4baf6c7816 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -53,7 +53,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -106,7 +106,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -153,7 +153,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -204,7 +204,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -255,7 +255,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -307,7 +307,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -345,7 +345,7 @@ public async Task Cannot_delete_resource_for_href_element() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -379,7 +379,7 @@ public async Task Cannot_delete_resource_for_missing_ref_element() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -417,7 +417,7 @@ public async Task Cannot_delete_resource_for_missing_type() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -456,7 +456,7 @@ public async Task Cannot_delete_resource_for_unknown_type() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -494,7 +494,7 @@ public async Task Cannot_delete_resource_for_missing_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -535,7 +535,7 @@ public async Task Cannot_delete_resource_for_unknown_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -576,7 +576,7 @@ public async Task Cannot_delete_resource_for_incompatible_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -616,7 +616,7 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs index 792e565f3a..a6e162f72a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs @@ -81,7 +81,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); @@ -157,7 +157,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs index 4fa5027418..db6ee06bbf 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs @@ -74,7 +74,7 @@ public async Task Create_resource_with_side_effects_returns_relative_links() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs index 05b7c22ae1..39768280bd 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs @@ -78,7 +78,7 @@ public async Task Can_create_resource_with_ManyToOne_relationship_using_local_ID (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); @@ -175,7 +175,7 @@ public async Task Can_create_resource_with_OneToMany_relationship_using_local_ID (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); @@ -271,7 +271,7 @@ public async Task Can_create_resource_with_ManyToMany_relationship_using_local_I (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); @@ -358,7 +358,7 @@ public async Task Cannot_consume_local_ID_that_is_assigned_in_same_operation() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -425,7 +425,7 @@ public async Task Cannot_reassign_local_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -485,7 +485,7 @@ public async Task Can_update_resource_using_local_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); @@ -605,7 +605,7 @@ public async Task Can_update_resource_with_relationships_using_local_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(4); @@ -724,7 +724,7 @@ public async Task Can_create_ManyToOne_relationship_using_local_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(3); @@ -826,7 +826,7 @@ public async Task Can_create_OneToMany_relationship_using_local_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(3); @@ -928,7 +928,7 @@ public async Task Can_create_ManyToMany_relationship_using_local_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(3); @@ -1052,7 +1052,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(3); @@ -1176,7 +1176,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(3); @@ -1300,7 +1300,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(3); @@ -1446,7 +1446,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(4); @@ -1603,7 +1603,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(4); @@ -1741,7 +1741,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(4); @@ -1810,7 +1810,7 @@ public async Task Can_delete_resource_using_local_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); @@ -1868,7 +1868,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -1918,7 +1918,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_data_element() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -1982,7 +1982,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -2045,7 +2045,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_elemen (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -2111,7 +2111,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_array( (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -2176,7 +2176,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_same_operation() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -2240,7 +2240,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -2301,7 +2301,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_data_element() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -2382,7 +2382,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -2461,7 +2461,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -2537,7 +2537,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs index 8fcda54494..12875231b6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs @@ -81,7 +81,7 @@ public async Task Returns_resource_meta_in_create_resource_with_side_effects() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); @@ -153,7 +153,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs index a41a2bb22c..1dec130347 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations.Meta; public sealed class AtomicResponseMeta : IResponseMeta { - public IReadOnlyDictionary GetMeta() + public IDictionary GetMeta() { return new Dictionary { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs index 327ff8e18c..ab084a0e90 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs @@ -58,7 +58,7 @@ public async Task Returns_top_level_meta_in_create_resource_with_side_effects() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.ShouldHaveCount(3); @@ -124,7 +124,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.ShouldHaveCount(3); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs index 19c9b2d872..f871e90238 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs @@ -73,7 +73,7 @@ public async Task Logs_at_error_level_on_unhandled_exception() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.ShouldHaveCount(1); @@ -117,7 +117,7 @@ public async Task Logs_at_info_level_on_invalid_request_body() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs index aaf3722093..ee9d144123 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs @@ -27,7 +27,7 @@ public async Task Cannot_process_for_missing_request_body() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, null!); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -51,7 +51,7 @@ public async Task Cannot_process_for_null_request_body() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -75,7 +75,7 @@ public async Task Cannot_process_for_broken_JSON_request_body() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -104,7 +104,7 @@ public async Task Cannot_process_for_missing_operations_array() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -131,7 +131,7 @@ public async Task Cannot_process_empty_operations_array() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -161,7 +161,7 @@ public async Task Cannot_process_null_operation() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -202,7 +202,7 @@ public async Task Cannot_process_for_unknown_operation_code() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs index 314e14746e..925cd2e551 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs @@ -87,7 +87,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Should().BeJson(@"{ ""jsonapi"": { @@ -155,7 +155,7 @@ public async Task Includes_version_with_ext_on_error_in_operations_endpoint() (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); string errorId = JsonApiStringConverter.ExtractErrorId(responseDocument); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs index 818fda8a70..f24e25a216 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs @@ -63,7 +63,7 @@ public async Task Cannot_process_more_operations_than_maximum() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -118,7 +118,7 @@ public async Task Can_process_operations_same_as_maximum() (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); } [Fact] @@ -158,6 +158,6 @@ public async Task Can_process_high_number_of_operations_when_unconstrained() (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs index 9c69af2f71..0eadc45f2c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs @@ -48,7 +48,7 @@ public async Task Cannot_create_resource_with_multiple_violations() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(2); @@ -119,7 +119,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -173,7 +173,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(2); @@ -231,7 +231,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -293,7 +293,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -347,7 +347,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -404,7 +404,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -460,7 +460,7 @@ public async Task Validates_all_operations_before_execution_starts() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(2); @@ -544,7 +544,7 @@ public async Task Does_not_exceed_MaxModelValidationErrors() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(3); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs index cf0db00e3e..640e6ac3fe 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs @@ -63,7 +63,7 @@ public async Task Cannot_include_on_operations_endpoint() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -103,7 +103,7 @@ public async Task Cannot_filter_on_operations_endpoint() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -143,7 +143,7 @@ public async Task Cannot_sort_on_operations_endpoint() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -183,7 +183,7 @@ public async Task Cannot_use_pagination_number_on_operations_endpoint() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -223,7 +223,7 @@ public async Task Cannot_use_pagination_size_on_operations_endpoint() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -263,7 +263,7 @@ public async Task Cannot_use_sparse_fieldset_on_operations_endpoint() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -297,7 +297,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(musicTracks[2].StringId); @@ -334,7 +334,7 @@ public async Task Cannot_use_Queryable_handler_on_operations_endpoint() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs index acbd3e0895..27e44ec234 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs @@ -86,7 +86,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); @@ -179,7 +179,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -238,7 +238,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); @@ -330,7 +330,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -380,7 +380,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs index a25100b414..f70a289ba1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs @@ -84,7 +84,7 @@ public async Task Hides_text_in_create_resource_with_side_effects() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); @@ -163,7 +163,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(2); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs index 8e4baf0cef..82646686d4 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs @@ -86,7 +86,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -166,7 +166,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs index 711c45de00..97f0e08ff5 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs @@ -61,7 +61,7 @@ public async Task Cannot_use_non_transactional_repository() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -104,7 +104,7 @@ public async Task Cannot_use_transactional_repository_without_active_transaction (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -147,7 +147,7 @@ public async Task Cannot_use_distributed_transaction() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs index c50c52e08e..2302ac20f7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs @@ -19,7 +19,7 @@ public Task CountAsync(FilterExpression? filter, CancellationToken cancella throw new NotImplementedException(); } - public Task GetForCreateAsync(int id, CancellationToken cancellationToken) + public Task GetForCreateAsync(Type resourceClrType, int id, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -39,7 +39,7 @@ public Task UpdateAsync(Performer resourceFromRequest, Performer resourceFromDat throw new NotImplementedException(); } - public Task DeleteAsync(int id, CancellationToken cancellationToken) + public Task DeleteAsync(Performer? resourceFromDatabase, int id, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -49,7 +49,7 @@ public Task SetRelationshipAsync(Performer leftResource, object? rightValue, Can throw new NotImplementedException(); } - public Task AddToToManyRelationshipAsync(int leftId, ISet rightResourceIds, CancellationToken cancellationToken) + public Task AddToToManyRelationshipAsync(Performer? leftResource, int leftId, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index bd97c23411..fc13524e8a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -60,7 +60,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.ShouldHaveCount(1); @@ -138,7 +138,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -218,7 +218,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -255,7 +255,7 @@ public async Task Cannot_add_for_href_element() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -294,7 +294,7 @@ public async Task Cannot_add_for_missing_type_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -334,7 +334,7 @@ public async Task Cannot_add_for_unknown_type_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -373,7 +373,7 @@ public async Task Cannot_add_for_missing_ID_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -431,7 +431,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -472,7 +472,7 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -511,7 +511,7 @@ public async Task Cannot_add_for_missing_relationship_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -551,7 +551,7 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -599,7 +599,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -648,7 +648,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -699,7 +699,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -746,7 +746,7 @@ public async Task Cannot_add_for_missing_type_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -794,7 +794,7 @@ public async Task Cannot_add_for_unknown_type_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -841,7 +841,7 @@ public async Task Cannot_add_for_missing_ID_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -890,7 +890,7 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -957,7 +957,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(2); @@ -1019,14 +1019,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.ShouldHaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); - error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); + error.Detail.Should().Be("Type 'playlists' is not convertible to type 'performers' of relationship 'performers'."); error.Source.ShouldNotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty()); @@ -1069,7 +1069,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index 1e6787a0d7..d015cae3fd 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -60,7 +60,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.ShouldHaveCount(1); @@ -136,7 +136,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -215,7 +215,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -254,7 +254,7 @@ public async Task Cannot_remove_for_href_element() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -293,7 +293,7 @@ public async Task Cannot_remove_for_missing_type_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -333,7 +333,7 @@ public async Task Cannot_remove_for_unknown_type_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -372,7 +372,7 @@ public async Task Cannot_remove_for_missing_ID_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -430,7 +430,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -471,7 +471,7 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -511,7 +511,7 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -559,7 +559,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -608,7 +608,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -659,7 +659,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -706,7 +706,7 @@ public async Task Cannot_remove_for_missing_type_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -754,7 +754,7 @@ public async Task Cannot_remove_for_unknown_type_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -801,7 +801,7 @@ public async Task Cannot_remove_for_missing_ID_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -850,7 +850,7 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -917,7 +917,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(2); @@ -979,14 +979,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.ShouldHaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); - error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); + error.Detail.Should().Be("Type 'playlists' is not convertible to type 'performers' of relationship 'performers'."); error.Source.ShouldNotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty()); @@ -1030,7 +1030,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index ee3ea791a5..faf8a9cb8c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -57,7 +57,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -110,7 +110,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -179,7 +179,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -249,7 +249,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -289,7 +289,7 @@ public async Task Cannot_replace_for_href_element() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -328,7 +328,7 @@ public async Task Cannot_replace_for_missing_type_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -368,7 +368,7 @@ public async Task Cannot_replace_for_unknown_type_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -407,7 +407,7 @@ public async Task Cannot_replace_for_missing_ID_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -465,7 +465,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -523,7 +523,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -564,7 +564,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -604,7 +604,7 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -652,7 +652,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -701,7 +701,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -752,7 +752,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -799,7 +799,7 @@ public async Task Cannot_replace_for_missing_type_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -847,7 +847,7 @@ public async Task Cannot_replace_for_unknown_type_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -894,7 +894,7 @@ public async Task Cannot_replace_for_missing_ID_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -943,7 +943,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1010,7 +1010,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(2); @@ -1072,7 +1072,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1128,14 +1128,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.ShouldHaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); - error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); + error.Detail.Should().Be("Type 'playlists' is not convertible to type 'performers' of relationship 'performers'."); error.Source.ShouldNotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty()); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index b66a5dfc26..4b650a3d85 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -57,7 +57,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -110,7 +110,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -163,7 +163,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -219,7 +219,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -273,7 +273,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -327,7 +327,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -384,7 +384,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -444,7 +444,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -504,7 +504,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -542,7 +542,7 @@ public async Task Cannot_create_for_href_element() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -581,7 +581,7 @@ public async Task Cannot_create_for_missing_type_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -621,7 +621,7 @@ public async Task Cannot_create_for_unknown_type_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -660,7 +660,7 @@ public async Task Cannot_create_for_missing_ID_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -715,7 +715,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -768,7 +768,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -809,7 +809,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -849,7 +849,7 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -897,7 +897,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -953,7 +953,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -997,7 +997,7 @@ public async Task Cannot_create_for_missing_type_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1042,7 +1042,7 @@ public async Task Cannot_create_for_unknown_type_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1086,7 +1086,7 @@ public async Task Cannot_create_for_missing_ID_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1132,7 +1132,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1187,7 +1187,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -1240,7 +1240,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1293,14 +1293,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.ShouldHaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); - error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'."); + error.Detail.Should().Be("Type 'playlists' is not convertible to type 'lyrics' of relationship 'lyric'."); error.Source.ShouldNotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty()); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 98b8892800..fa801c67c1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -62,7 +62,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -120,7 +120,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -194,7 +194,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -269,7 +269,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -327,7 +327,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -381,7 +381,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -437,7 +437,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -489,7 +489,7 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -542,7 +542,7 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -594,7 +594,7 @@ public async Task Cannot_replace_for_missing_ID_in_relationship_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -648,7 +648,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -720,7 +720,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(2); @@ -787,14 +787,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.ShouldHaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); - error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); + error.Detail.Should().Be("Type 'playlists' is not convertible to type 'performers' of relationship 'performers'."); error.Source.ShouldNotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty()); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index f8051e369c..336b7d5621 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -81,7 +81,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -142,7 +142,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -198,7 +198,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -254,7 +254,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -311,7 +311,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -372,7 +372,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); } @@ -418,7 +418,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -483,7 +483,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -540,7 +540,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -596,7 +596,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Results.ShouldHaveCount(1); @@ -629,7 +629,7 @@ public async Task Cannot_update_resource_for_href_element() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -686,7 +686,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -735,7 +735,7 @@ public async Task Cannot_update_resource_for_missing_type_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -784,7 +784,7 @@ public async Task Cannot_update_resource_for_missing_ID_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -835,7 +835,7 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_ref() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -869,7 +869,7 @@ public async Task Cannot_update_resource_for_missing_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -904,7 +904,7 @@ public async Task Cannot_update_resource_for_null_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -958,7 +958,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1002,7 +1002,7 @@ public async Task Cannot_update_resource_for_missing_type_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1046,7 +1046,7 @@ public async Task Cannot_update_resource_for_missing_ID_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1092,7 +1092,7 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1142,14 +1142,14 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.ShouldHaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); - error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers'."); + error.Detail.Should().Be("Type 'playlists' is not convertible to type 'performers'."); error.Source.ShouldNotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty()); @@ -1195,7 +1195,7 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.ShouldHaveCount(1); @@ -1245,7 +1245,7 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.ShouldHaveCount(1); @@ -1297,7 +1297,7 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1349,7 +1349,7 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1394,7 +1394,7 @@ public async Task Cannot_update_resource_for_unknown_type() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1441,7 +1441,7 @@ public async Task Cannot_update_resource_for_unknown_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -1490,7 +1490,7 @@ public async Task Cannot_update_resource_for_incompatible_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1541,7 +1541,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1592,7 +1592,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1643,7 +1643,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1694,7 +1694,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -1784,7 +1784,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index efa3813d74..3996983042 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -62,7 +62,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -120,7 +120,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -178,7 +178,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -239,7 +239,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -298,7 +298,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -357,7 +357,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -419,7 +419,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -484,7 +484,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -549,7 +549,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -603,7 +603,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -656,7 +656,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -717,7 +717,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -766,7 +766,7 @@ public async Task Cannot_create_for_missing_type_in_relationship_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -816,7 +816,7 @@ public async Task Cannot_create_for_unknown_type_in_relationship_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -865,7 +865,7 @@ public async Task Cannot_create_for_missing_ID_in_relationship_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -916,7 +916,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -976,7 +976,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -1034,14 +1034,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.ShouldHaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); - error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'."); + error.Detail.Should().Be("Type 'playlists' is not convertible to type 'lyrics' of relationship 'lyric'."); error.Source.ShouldNotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty()); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Blobs/BlobTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Blobs/BlobTests.cs index 67ca34d83c..4d21e284e8 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Blobs/BlobTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Blobs/BlobTests.cs @@ -43,7 +43,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("imageContainers"); @@ -80,7 +80,7 @@ public async Task Can_create_resource() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("imageContainers"); @@ -136,7 +136,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("imageContainers"); @@ -187,7 +187,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("imageContainers"); @@ -236,7 +236,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("imageContainers"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarCompositeKeyAwareRepository.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarCompositeKeyAwareRepository.cs index 4009f1dd82..989967bc10 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarCompositeKeyAwareRepository.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarCompositeKeyAwareRepository.cs @@ -41,9 +41,11 @@ private void RecursiveRewriteFilterInLayer(QueryLayer queryLayer) queryLayer.Sort = (SortExpression?)_writer.Visit(queryLayer.Sort, null); } - if (queryLayer.Projection != null) + if (queryLayer.Selection is { IsEmpty: false }) { - foreach (QueryLayer? nextLayer in queryLayer.Projection.Values.Where(layer => layer != null)) + foreach (QueryLayer? nextLayer in queryLayer.Selection.GetResourceTypes() + .Select(resourceType => queryLayer.Selection.GetOrCreateSelectors(resourceType)) + .SelectMany(selectors => selectors.Select(selector => selector.Value).Where(layer => layer != null))) { RecursiveRewriteFilterInLayer(nextLayer!); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs index 981190dc72..185367930d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs @@ -51,7 +51,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(car.StringId); @@ -76,7 +76,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(car.StringId); @@ -101,7 +101,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(car.StringId); @@ -126,7 +126,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(car.StringId); @@ -177,7 +177,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -230,7 +230,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -279,7 +279,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -323,7 +323,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -368,7 +368,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -420,7 +420,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -467,7 +467,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -496,7 +496,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs index caa29722c5..20fc6f8032 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs @@ -30,7 +30,7 @@ public async Task Permits_no_Accept_headers() (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); } [Fact] @@ -63,7 +63,7 @@ public async Task Permits_no_Accept_headers_at_operations_endpoint() (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); } [Fact] @@ -82,7 +82,7 @@ public async Task Permits_global_wildcard_in_Accept_headers() (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route, setRequestHeaders); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); } [Fact] @@ -101,7 +101,7 @@ public async Task Permits_application_wildcard_in_Accept_headers() (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route, setRequestHeaders); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); } [Fact] @@ -123,7 +123,7 @@ public async Task Permits_JsonApi_without_parameters_in_Accept_headers() (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route, setRequestHeaders); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); } [Fact] @@ -165,7 +165,7 @@ public async Task Permits_JsonApi_with_AtomicOperations_extension_in_Accept_head (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType, setRequestHeaders); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); } [Fact] @@ -187,7 +187,7 @@ public async Task Denies_JsonApi_with_parameters_in_Accept_headers() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route, setRequestHeaders); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotAcceptable); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotAcceptable); responseDocument.Errors.ShouldHaveCount(1); @@ -235,7 +235,7 @@ public async Task Denies_JsonApi_in_Accept_headers_at_operations_endpoint() await _testContext.ExecutePostAsync(route, requestBody, contentType, setRequestHeaders); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotAcceptable); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotAcceptable); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs index 70fd03c453..793dee05c8 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs @@ -29,7 +29,7 @@ public async Task Returns_JsonApi_ContentType_header() (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); httpResponse.Content.Headers.ContentType.ShouldNotBeNull(); httpResponse.Content.Headers.ContentType.ToString().Should().Be(HeaderConstants.MediaType); } @@ -63,7 +63,7 @@ public async Task Returns_JsonApi_ContentType_header_with_AtomicOperations_exten (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); httpResponse.Content.Headers.ContentType.ShouldNotBeNull(); httpResponse.Content.Headers.ContentType.ToString().Should().Be(HeaderConstants.AtomicOperationsMediaType); } @@ -91,7 +91,7 @@ public async Task Denies_unknown_ContentType_header() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.ShouldHaveCount(1); @@ -127,7 +127,7 @@ public async Task Permits_JsonApi_ContentType_header() (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); } [Fact] @@ -160,7 +160,7 @@ public async Task Permits_JsonApi_ContentType_header_with_AtomicOperations_exten (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); } [Fact] @@ -186,7 +186,7 @@ public async Task Denies_JsonApi_ContentType_header_with_profile() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.ShouldHaveCount(1); @@ -221,7 +221,7 @@ public async Task Denies_JsonApi_ContentType_header_with_extension() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.ShouldHaveCount(1); @@ -256,7 +256,7 @@ public async Task Denies_JsonApi_ContentType_header_with_AtomicOperations_extens (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.ShouldHaveCount(1); @@ -291,7 +291,7 @@ public async Task Denies_JsonApi_ContentType_header_with_CharSet() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.ShouldHaveCount(1); @@ -326,7 +326,7 @@ public async Task Denies_JsonApi_ContentType_header_with_unknown_parameter() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.ShouldHaveCount(1); @@ -369,7 +369,7 @@ public async Task Denies_JsonApi_ContentType_header_at_operations_endpoint() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs index 94e9a12b85..b11c31623c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs @@ -35,7 +35,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(toothbrush.StringId); @@ -51,7 +51,7 @@ public async Task Converts_empty_ActionResult_to_error_collection() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -71,7 +71,7 @@ public async Task Converts_ActionResult_with_error_object_to_error_collection() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -91,7 +91,7 @@ public async Task Cannot_convert_ActionResult_with_string_parameter_to_error_col (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.ShouldHaveCount(1); @@ -111,7 +111,7 @@ public async Task Converts_ObjectResult_with_error_object_to_error_collection() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadGateway); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadGateway); responseDocument.Errors.ShouldHaveCount(1); @@ -131,7 +131,7 @@ public async Task Converts_ObjectResult_with_error_objects_to_error_collection() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(3); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs index ee5c6c2da7..25404e25d2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs @@ -27,7 +27,7 @@ public async Task ApiController_attribute_transforms_NotFound_action_result_with (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs index a4a98592d9..572f2baeed 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs @@ -40,7 +40,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("towns"); @@ -82,7 +82,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(5); responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Type == "towns"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/BuildingRepository.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/BuildingRepository.cs index 572a7bd3e6..888f060ca1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/BuildingRepository.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/BuildingRepository.cs @@ -17,9 +17,9 @@ public BuildingRepository(ITargetedFields targetedFields, IDbContextResolver dbC { } - public override async Task GetForCreateAsync(int id, CancellationToken cancellationToken) + public override async Task GetForCreateAsync(Type resourceClrType, int id, CancellationToken cancellationToken) { - Building building = await base.GetForCreateAsync(id, cancellationToken); + Building building = await base.GetForCreateAsync(resourceClrType, id, cancellationToken); // Must ensure that an instance exists for this required relationship, so that POST Resource succeeds. building.PrimaryDoor = new Door diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs index e12167ad3a..b65d9765f7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs @@ -49,7 +49,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(building.StringId); @@ -85,7 +85,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(street.StringId); @@ -116,7 +116,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(street.StringId); @@ -148,7 +148,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(state.StringId); @@ -191,7 +191,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(state.Cities[0].StringId); @@ -232,7 +232,7 @@ public async Task Can_create_resource() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("number").With(value => value.Should().Be(newBuilding.Number)); @@ -303,7 +303,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -362,7 +362,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -393,7 +393,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs index 13f69d94b4..53a2415627 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs @@ -66,7 +66,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Gone); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Gone); responseDocument.Errors.ShouldHaveCount(1); @@ -103,7 +103,7 @@ public async Task Logs_and_produces_error_response_on_deserialization_failure() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -150,7 +150,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/HitCountingResourceDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/HitCountingResourceDefinition.cs index 57098822a1..8132728c90 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/HitCountingResourceDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/HitCountingResourceDefinition.cs @@ -129,7 +129,7 @@ public override Task OnSetToManyRelationshipAsync(TResource leftResource, HasMan return base.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIds, writeOperation, cancellationToken); } - public override Task OnAddToRelationshipAsync(TId leftResourceId, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + public override Task OnAddToRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, CancellationToken cancellationToken) { if (ExtensibilityPointsToTrack.HasFlag(ResourceDefinitionExtensibilityPoints.OnAddToRelationshipAsync)) @@ -137,7 +137,7 @@ public override Task OnAddToRelationshipAsync(TId leftResourceId, HasManyAttribu _hitCounter.TrackInvocation(ResourceDefinitionExtensibilityPoints.OnAddToRelationshipAsync); } - return base.OnAddToRelationshipAsync(leftResourceId, hasManyRelationship, rightResourceIds, cancellationToken); + return base.OnAddToRelationshipAsync(leftResource, hasManyRelationship, rightResourceIds, cancellationToken); } public override Task OnRemoveFromRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingStartup.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingStartup.cs index 24546c2062..a22dfe5954 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingStartup.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingStartup.cs @@ -1,9 +1,7 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using TestBuildingBlocks; namespace JsonApiDotNetCoreTests.IntegrationTests.HostingInIIS; @@ -20,10 +18,10 @@ protected override void SetJsonApiOptions(JsonApiOptions options) options.IncludeTotalResourceCount = true; } - public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory) + public override void Configure(IApplicationBuilder app) { app.UsePathBase("/iis-application-virtual-directory"); - base.Configure(app, environment, loggerFactory); + base.Configure(app); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingTests.cs index c6e4ea363b..872cd44c3a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingTests.cs @@ -41,7 +41,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -108,7 +108,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs index e14f5c616e..e9ed2f789c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs @@ -39,7 +39,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[1].StringId); @@ -65,7 +65,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[1].StringId); @@ -81,7 +81,7 @@ public async Task Cannot_get_primary_resource_for_invalid_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); @@ -110,7 +110,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(card.StringId); @@ -135,7 +135,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(2); responseDocument.Data.ManyValue[0].Id.Should().Be(account.Cards[0].StringId); @@ -161,7 +161,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(account.StringId); @@ -191,7 +191,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(account.Cards[0].StringId); @@ -240,7 +240,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("ownerName").With(value => value.Should().Be(newCard.OwnerName)); @@ -313,7 +313,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -363,7 +363,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -406,7 +406,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -437,7 +437,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -462,7 +462,7 @@ public async Task Cannot_delete_unknown_resource() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs index 5a07c9ec99..7df5b15a74 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs @@ -41,7 +41,7 @@ public async Task Cannot_create_resource_with_omitted_required_attribute() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -76,7 +76,7 @@ public async Task Cannot_create_resource_with_null_for_required_attribute_value( (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -111,7 +111,7 @@ public async Task Cannot_create_resource_with_invalid_attribute_value() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -148,7 +148,7 @@ public async Task Can_create_resource_with_valid_attribute_value() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("directoryName").With(value => value.Should().Be(newDirectory.Name)); @@ -178,7 +178,7 @@ public async Task Cannot_create_resource_with_multiple_violations() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(2); @@ -219,7 +219,7 @@ public async Task Does_not_exceed_MaxModelValidationErrors() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(3); @@ -313,7 +313,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("directoryName").With(value => value.Should().Be(newDirectory.Name)); @@ -351,7 +351,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); } @@ -389,7 +389,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); } @@ -426,7 +426,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(2); @@ -476,7 +476,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -521,7 +521,7 @@ public async Task Cannot_update_resource_with_invalid_ID() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(2); @@ -573,7 +573,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); } @@ -652,7 +652,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); } @@ -703,7 +703,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); } @@ -749,7 +749,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); } @@ -784,7 +784,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); } @@ -822,7 +822,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); } @@ -858,7 +858,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/NoModelStateValidationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/NoModelStateValidationTests.cs index bef4748d69..671123930e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/NoModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/NoModelStateValidationTests.cs @@ -44,7 +44,7 @@ public async Task Can_create_resource_with_invalid_attribute_value() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("directoryName").With(value => value.Should().Be("!@#$%^&*().-")); @@ -81,7 +81,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); } @@ -121,7 +121,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowTests.cs index 002d598003..4653118ab6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowTests.cs @@ -45,7 +45,7 @@ public async Task Can_create_in_valid_stage() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); } @@ -72,7 +72,7 @@ public async Task Cannot_create_in_invalid_stage() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -118,7 +118,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -164,7 +164,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs index 799942bea9..62ba148dc4 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs @@ -52,7 +52,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -95,7 +95,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -161,7 +161,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -205,7 +205,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -253,7 +253,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -287,7 +287,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -348,7 +348,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -431,7 +431,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs index 6fa391fd22..0d496f18eb 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs @@ -52,7 +52,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -95,7 +95,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -161,7 +161,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -205,7 +205,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -253,7 +253,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -287,7 +287,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -348,7 +348,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -431,7 +431,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionIncludeTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionIncludeTests.cs new file mode 100644 index 0000000000..6b11fb5403 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionIncludeTests.cs @@ -0,0 +1,65 @@ +using System.Net; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using TestBuildingBlocks; +using Xunit; + +namespace JsonApiDotNetCoreTests.IntegrationTests.Links; + +public sealed class LinkInclusionIncludeTests : IClassFixture, LinksDbContext>> +{ + private readonly IntegrationTestContext, LinksDbContext> _testContext; + private readonly LinksFakers _fakers = new(); + + public LinkInclusionIncludeTests(IntegrationTestContext, LinksDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + } + + [Fact] + public async Task Hides_links_for_unregistered_controllers() + { + // Arrange + PhotoLocation location = _fakers.PhotoLocation.Generate(); + location.Photo = _fakers.Photo.Generate(); + location.Album = _fakers.PhotoAlbum.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.PhotoLocations.Add(location); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/photoLocations/{location.StringId}?include=photo,album"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.SingleValue.ShouldNotBeNull(); + + responseDocument.Data.SingleValue.Relationships.ShouldContainKey("photo").With(value => + { + value.ShouldNotBeNull(); + value.Links.ShouldNotBeNull(); + }); + + responseDocument.Included.ShouldHaveCount(2); + + responseDocument.Included.Should().ContainSingle(resource => resource.Type == "photos").Subject.With(resource => + { + resource.Links.Should().BeNull(); + resource.Relationships.Should().BeNull(); + }); + + responseDocument.Included.Should().ContainSingle(resource => resource.Type == "photoAlbums").Subject.With(resource => + { + resource.Links.Should().BeNull(); + resource.Relationships.Should().BeNull(); + }); + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionTests.cs index 128a19efb9..1405d648f2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionTests.cs @@ -17,6 +17,7 @@ public LinkInclusionTests(IntegrationTestContext testContext.UseController(); testContext.UseController(); + testContext.UseController(); } [Fact] @@ -39,7 +40,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.Should().BeNull(); @@ -110,7 +111,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs index be55eec1d1..a51f522b82 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs @@ -52,7 +52,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -95,7 +95,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -161,7 +161,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -205,7 +205,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -253,7 +253,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -287,7 +287,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -348,7 +348,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -431,7 +431,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs index 93e85cd0e1..cf614fc8f8 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs @@ -52,7 +52,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -95,7 +95,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -161,7 +161,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -205,7 +205,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -253,7 +253,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -287,7 +287,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -348,7 +348,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); @@ -431,7 +431,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Logging/LoggingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Logging/LoggingTests.cs index f7d800a2af..d297467bc4 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Logging/LoggingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Logging/LoggingTests.cs @@ -62,7 +62,7 @@ public async Task Logs_request_body_at_Trace_level() (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); loggerFactory.Logger.Messages.ShouldNotBeEmpty(); @@ -84,7 +84,7 @@ public async Task Logs_response_body_at_Trace_level() (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); loggerFactory.Logger.Messages.ShouldNotBeEmpty(); @@ -108,7 +108,7 @@ public async Task Logs_invalid_request_body_error_at_Information_level() (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); loggerFactory.Logger.Messages.ShouldNotBeEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResourceMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResourceMetaTests.cs index 1ee473bffd..ba15de73d7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResourceMetaTests.cs @@ -53,7 +53,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(3); responseDocument.Data.ManyValue[0].Meta.ShouldContainKey("hasHighPriority"); @@ -91,7 +91,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Included.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs index 14818db0d5..5b86a62322 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs @@ -43,7 +43,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Should().BeJson(@"{ ""links"": { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/SupportResponseMeta.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/SupportResponseMeta.cs index 8e3b27b286..367a046a76 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/SupportResponseMeta.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/SupportResponseMeta.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.Meta; public sealed class SupportResponseMeta : IResponseMeta { - public IReadOnlyDictionary GetMeta() + public IDictionary GetMeta() { return new Dictionary { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/TopLevelCountTests.cs index 20de051454..eee8fa75e3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/TopLevelCountTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/TopLevelCountTests.cs @@ -50,7 +50,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.ShouldNotBeNull(); @@ -76,7 +76,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.ShouldNotBeNull(); @@ -111,7 +111,7 @@ public async Task Hides_resource_count_in_create_resource_response() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Meta.Should().BeNull(); } @@ -149,7 +149,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.Should().BeNull(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs index e35efaeb52..7421c06256 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs @@ -37,7 +37,7 @@ public async Task Create_group_sends_messages() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("name").With(value => value.Should().Be(newGroupName)); @@ -115,7 +115,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("name").With(value => value.Should().Be(newGroupName)); @@ -182,7 +182,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -266,7 +266,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -315,7 +315,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -353,7 +353,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -427,7 +427,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -501,7 +501,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -563,12 +563,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { + (typeof(DomainGroup), ResourceDefinitionExtensibilityPoints.OnPrepareWriteAsync), (typeof(DomainGroup), ResourceDefinitionExtensibilityPoints.OnRemoveFromRelationshipAsync), (typeof(DomainGroup), ResourceDefinitionExtensibilityPoints.OnWritingAsync), (typeof(DomainGroup), ResourceDefinitionExtensibilityPoints.OnWriteSucceededAsync) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs index 35b2666c02..1a53699b03 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs @@ -39,7 +39,7 @@ public async Task Create_user_sends_messages() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("loginName").With(value => value.Should().Be(newLoginName)); @@ -108,7 +108,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("loginName").With(value => value.Should().Be(newLoginName)); @@ -174,7 +174,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -242,7 +242,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -314,7 +314,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -388,7 +388,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -434,7 +434,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -472,7 +472,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -519,7 +519,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -569,7 +569,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -621,7 +621,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs index 89afb428f1..fe7eabd112 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs @@ -51,7 +51,7 @@ public async Task Does_not_send_message_on_write_error() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -92,7 +92,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.ServiceUnavailable); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.ServiceUnavailable); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/MessagingGroupDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/MessagingGroupDefinition.cs index ce8e10c62e..9365ff08a0 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/MessagingGroupDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/MessagingGroupDefinition.cs @@ -80,10 +80,10 @@ public override async Task OnSetToManyRelationshipAsync(DomainGroup group, HasMa } } - public override async Task OnAddToRelationshipAsync(Guid groupId, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + public override async Task OnAddToRelationshipAsync(DomainGroup group, HasManyAttribute hasManyRelationship, ISet rightResourceIds, CancellationToken cancellationToken) { - await base.OnAddToRelationshipAsync(groupId, hasManyRelationship, rightResourceIds, cancellationToken); + await base.OnAddToRelationshipAsync(group, hasManyRelationship, rightResourceIds, cancellationToken); if (hasManyRelationship.Property.Name == nameof(DomainGroup.Users)) { @@ -98,11 +98,11 @@ public override async Task OnAddToRelationshipAsync(Guid groupId, HasManyAttribu if (beforeUser.Group == null) { - content = new UserAddedToGroupContent(beforeUser.Id, groupId); + content = new UserAddedToGroupContent(beforeUser.Id, group.Id); } - else if (beforeUser.Group != null && beforeUser.Group.Id != groupId) + else if (beforeUser.Group != null && beforeUser.Group.Id != group.Id) { - content = new UserMovedToGroupContent(beforeUser.Id, beforeUser.Group.Id, groupId); + content = new UserMovedToGroupContent(beforeUser.Id, beforeUser.Group.Id, group.Id); } if (content != null) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs index a5c61d1f70..cc5b5e84ab 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs @@ -42,7 +42,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("name").With(value => value.Should().Be(newGroupName)); @@ -124,7 +124,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("name").With(value => value.Should().Be(newGroupName)); @@ -195,7 +195,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -283,7 +283,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -336,7 +336,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -378,7 +378,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -456,7 +456,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -534,7 +534,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -600,12 +600,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { + (typeof(DomainGroup), ResourceDefinitionExtensibilityPoints.OnPrepareWriteAsync), (typeof(DomainGroup), ResourceDefinitionExtensibilityPoints.OnRemoveFromRelationshipAsync), (typeof(DomainGroup), ResourceDefinitionExtensibilityPoints.OnWritingAsync), (typeof(DomainGroup), ResourceDefinitionExtensibilityPoints.OnWriteSucceededAsync) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs index ee8c6aaa58..5ec47bb34a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs @@ -44,7 +44,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("loginName").With(value => value.Should().Be(newLoginName)); @@ -117,7 +117,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("loginName").With(value => value.Should().Be(newLoginName)); @@ -187,7 +187,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -259,7 +259,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -335,7 +335,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -413,7 +413,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -463,7 +463,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -505,7 +505,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -556,7 +556,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -610,7 +610,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -666,7 +666,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs index a67c539c85..529fcdab6c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs @@ -77,7 +77,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs index 0ddbe8efd0..b2b100b0bd 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs @@ -63,7 +63,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(shops[1].StringId); @@ -93,7 +93,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(shops[1].StringId); @@ -123,7 +123,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Type.Should().Be("webShops"); @@ -153,7 +153,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -183,7 +183,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -213,7 +213,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -243,7 +243,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -273,7 +273,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -307,7 +307,7 @@ public async Task Can_create_resource() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Attributes.ShouldContainKey("url").With(value => value.Should().Be(newShopUrl)); @@ -372,7 +372,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -426,7 +426,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -471,7 +471,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -519,7 +519,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -575,7 +575,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -628,7 +628,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -663,7 +663,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -708,7 +708,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -743,7 +743,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -785,7 +785,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -830,7 +830,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -874,7 +874,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -916,7 +916,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -946,7 +946,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); @@ -978,7 +978,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.ShouldHaveCount(1); @@ -1009,7 +1009,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs index 1f9b9a7b97..ab0a4a4a7e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs @@ -40,7 +40,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(2); responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Type == "swimming-pools"); @@ -86,7 +86,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Type.Should().Be("water-slides"); @@ -118,7 +118,7 @@ public async Task Can_create_resource() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("swimming-pools"); @@ -163,7 +163,7 @@ public async Task Applies_casing_convention_on_error_stack_trace() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -204,7 +204,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs index 98334a432f..59d442fcba 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs @@ -40,7 +40,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(2); responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Type == "SwimmingPools"); @@ -85,7 +85,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); responseDocument.Data.ManyValue.ShouldHaveCount(1); responseDocument.Data.ManyValue[0].Type.Should().Be("WaterSlides"); @@ -117,7 +117,7 @@ public async Task Can_create_resource() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); responseDocument.Data.SingleValue.ShouldNotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("SwimmingPools"); @@ -162,7 +162,7 @@ public async Task Applies_casing_convention_on_error_stack_trace() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); @@ -203,7 +203,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.ShouldHaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/DuplicateResourceControllerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/DuplicateResourceControllerTests.cs index 7fec2a877e..b130523588 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/DuplicateResourceControllerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/DuplicateResourceControllerTests.cs @@ -22,9 +22,4 @@ public void Fails_at_startup_when_multiple_controllers_exist_for_same_resource_t // Assert action.Should().ThrowExactly().WithMessage("Multiple controllers found for resource type 'knownResources'."); } - - public override void Dispose() - { - // Prevents crash when test cleanup tries to access lazily constructed Factory. - } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/NonJsonApiControllerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/NonJsonApiControllerTests.cs index 9c3364f4c5..88e9ecfd82 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/NonJsonApiControllerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/NonJsonApiControllerTests.cs @@ -23,13 +23,13 @@ public async Task Get_skips_middleware_and_formatters() // Arrange using var request = new HttpRequestMessage(HttpMethod.Get, "/NonJsonApi"); - HttpClient client = _testContext.Factory.CreateClient(); + using HttpClient client = _testContext.Factory.CreateClient(); // Act - HttpResponseMessage httpResponse = await client.SendAsync(request); + using HttpResponseMessage httpResponse = await client.SendAsync(request); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); httpResponse.Content.Headers.ContentType.ShouldNotBeNull(); httpResponse.Content.Headers.ContentType.ToString().Should().Be("application/json; charset=utf-8"); @@ -52,13 +52,13 @@ public async Task Post_skips_middleware_and_formatters() } }; - HttpClient client = _testContext.Factory.CreateClient(); + using HttpClient client = _testContext.Factory.CreateClient(); // Act - HttpResponseMessage httpResponse = await client.SendAsync(request); + using HttpResponseMessage httpResponse = await client.SendAsync(request); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); httpResponse.Content.Headers.ContentType.ShouldNotBeNull(); httpResponse.Content.Headers.ContentType.ToString().Should().Be("text/plain; charset=utf-8"); @@ -72,13 +72,13 @@ public async Task Post_skips_error_handler() // Arrange using var request = new HttpRequestMessage(HttpMethod.Post, "/NonJsonApi"); - HttpClient client = _testContext.Factory.CreateClient(); + using HttpClient client = _testContext.Factory.CreateClient(); // Act - HttpResponseMessage httpResponse = await client.SendAsync(request); + using HttpResponseMessage httpResponse = await client.SendAsync(request); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); httpResponse.Content.Headers.ContentType.ShouldNotBeNull(); httpResponse.Content.Headers.ContentType.ToString().Should().Be("text/plain; charset=utf-8"); @@ -101,13 +101,13 @@ public async Task Put_skips_middleware_and_formatters() } }; - HttpClient client = _testContext.Factory.CreateClient(); + using HttpClient client = _testContext.Factory.CreateClient(); // Act - HttpResponseMessage httpResponse = await client.SendAsync(request); + using HttpResponseMessage httpResponse = await client.SendAsync(request); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); httpResponse.Content.Headers.ContentType.ShouldNotBeNull(); httpResponse.Content.Headers.ContentType.ToString().Should().Be("text/plain; charset=utf-8"); @@ -121,13 +121,13 @@ public async Task Patch_skips_middleware_and_formatters() // Arrange using var request = new HttpRequestMessage(HttpMethod.Patch, "/NonJsonApi?name=Janice"); - HttpClient client = _testContext.Factory.CreateClient(); + using HttpClient client = _testContext.Factory.CreateClient(); // Act - HttpResponseMessage httpResponse = await client.SendAsync(request); + using HttpResponseMessage httpResponse = await client.SendAsync(request); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); httpResponse.Content.Headers.ContentType.ShouldNotBeNull(); httpResponse.Content.Headers.ContentType.ToString().Should().Be("text/plain; charset=utf-8"); @@ -141,13 +141,13 @@ public async Task Delete_skips_middleware_and_formatters() // Arrange using var request = new HttpRequestMessage(HttpMethod.Delete, "/NonJsonApi"); - HttpClient client = _testContext.Factory.CreateClient(); + using HttpClient client = _testContext.Factory.CreateClient(); // Act - HttpResponseMessage httpResponse = await client.SendAsync(request); + using HttpResponseMessage httpResponse = await client.SendAsync(request); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); httpResponse.Content.Headers.ContentType.ShouldNotBeNull(); httpResponse.Content.Headers.ContentType.ToString().Should().Be("text/plain; charset=utf-8"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/UnknownResourceControllerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/UnknownResourceControllerTests.cs index 1cfb3cc34c..55d09dc2fe 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/UnknownResourceControllerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/UnknownResourceControllerTests.cs @@ -22,9 +22,4 @@ public void Fails_at_startup_when_using_controller_for_resource_type_that_is_not action.Should().ThrowExactly().WithMessage($"Controller '{typeof(UnknownResourcesController)}' " + $"depends on resource type '{typeof(UnknownResource)}', which does not exist in the resource graph."); } - - public override void Dispose() - { - // Prevents crash when test cleanup tries to access lazily constructed Factory. - } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Blog.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Blog.cs index 58a4546ef5..1ad9f4b526 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Blog.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Blog.cs @@ -17,6 +17,8 @@ public sealed class Blog : Identifiable [Attr(Capabilities = AttrCapabilities.All & ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))] public bool ShowAdvertisements => PlatformName.EndsWith("(using free account)", StringComparison.Ordinal); + public bool IsPublished { get; set; } + [HasMany] public IList Posts { get; set; } = new List(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/BlogPost.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/BlogPost.cs index a5edb3123f..a628cf9355 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/BlogPost.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/BlogPost.cs @@ -20,6 +20,9 @@ public sealed class BlogPost : Identifiable [HasOne] public WebAccount? Reviewer { get; set; } + [HasMany] + public ISet Contributors { get; set; } = new HashSet(); + [HasMany] public ISet