From 6d7eeff3fcf1f957eb12e4dc5625e0684e32500a Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 16 Sep 2021 13:04:04 +0200 Subject: [PATCH 1/7] Removed temporary workaround to start postgres service in cibuild (#1083) --- appveyor.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index a1c5da588b..9093e8854d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -34,10 +34,7 @@ for: only: - image: Visual Studio 2019 services: - - postgresql134 - # https://help.appveyor.com/discussions/problems/30239-postgres-fails-to-connect-after-version-change - init: - - net start postgresql-x64-13 + - postgresql13 # REF: https://github.com/docascode/docfx-seed/blob/master/appveyor.yml before_build: - pwsh: | From 4b487f04c6550be5642c23b514b17827691b3a5a Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 16 Sep 2021 17:34:56 +0200 Subject: [PATCH 2/7] Restored docs for HasManyThrough, which was removed in #1037 (#1084) --- docs/usage/resources/relationships.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/usage/resources/relationships.md b/docs/usage/resources/relationships.md index 869b38f97c..172a8788ec 100644 --- a/docs/usage/resources/relationships.md +++ b/docs/usage/resources/relationships.md @@ -34,6 +34,32 @@ public class Person : Identifiable The left side of this relationship is of type `Person` (public name: "persons") and the right side is of type `TodoItem` (public name: "todoItems"). +## HasManyThrough + +_removed since v5.0_ + +Earlier versions of Entity Framework Core (up to v5) [did not support](https://github.com/aspnet/EntityFrameworkCore/issues/1368) many-to-many relationships without a join entity. +For this reason, earlier versions of JsonApiDotNetCore filled this gap by allowing applications to declare a relationship as `HasManyThrough`, +which would expose the relationship to the client the same way as any other `HasMany` relationship. +However, under the covers it would use the join type and Entity Framework Core's APIs to get and set the relationship. + +```c# +public class Article : Identifiable +{ + // tells Entity Framework Core to ignore this property + [NotMapped] + + // tells JsonApiDotNetCore to use the join table below + [HasManyThrough(nameof(ArticleTags))] + public ICollection Tags { get; set; } + + // this is the Entity Framework Core navigation to the join table + public ICollection ArticleTags { get; set; } +} +``` + +The left side of this relationship is of type `Article` (public name: "articles") and the right side is of type `Tag` (public name: "tags"). + ## Name There are two ways the exposed relationship name is determined: From c933e8b56849c6cbe6791243668e3735d04491b0 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 17 Sep 2021 17:18:49 +0200 Subject: [PATCH 3/7] Use System.Text.Json (#1075) * Removed Serialization.Client.Internal * Removed undocumented ?nulls and ?defaults query string support * Refactor: use interpolated strings instead of concatenation * Updated tests to use string value for IDs; centralized pseudo-constants * Added tests for pascal casing * Optimized attribute/relationship lookups * Breaking: Made IResourceContextProvider.GetResourceContext() throw when not found; added TryGetResourceContext() that returns null * Optimized resource graph lookups * Breaking: Merged IResourceContextProvider into IResourceGraph * Switched to STJ in assertions Note we need JsonDateTimeOffsetFormatSpecifier now, because STJ never tries to infer the CLR type from JSON values between quotes, while Newtonsoft does. So Newtonsoft would convert both values to date/time, effectively hiding the textual difference that was always there. * Switched to STJ in rendering exception stack traces * Switched to STJ in rendering CLR objects as part of tracing. STJ properly handles self-referencing EF Core objects when enabling reference tracking, as opposed to Newtonsoft. * Switched to STJ in attribute change tracking. This used to take options into account, which is unneeded because we only care about whether there's a diff, not so much what that diff looks like. And we don't expect self-references here (it would have crashed in the past, and will now too). * Switched to STJ in Microservices example * Removed re-indent of response body on HTTP status code mismatch in tests, because we already use indenting in TestableStartup, so this is no longer needed. * Use STJ naming convention on special-cased code paths * Renamed RelationshipEntry to RelationshipObject, Error to ErrorObject * Fix broken test in cibuild * Fixed broken tests in cibuild due to different line endings * Package updates * Refactor serialization objects - Simplified error objects, so they are similar to the other serialization objects. This means no default instances, constructors (exception: ErrorObject) or conditional serialization logic. And explicit names to overrule naming conventions. And annotations to skip serialization when null. - Added missing members from JSON:API v1.1 spec: ErrorDocument.Meta, ErrorLinks.Type, ErrorSource.Header, ResourceIdentifierObject.Meta - Normalized collection types - Updated documentation: Link to v1.1 of JSON:API spec instead of copy/pasted text * Merged ErrorDocument and AtomicOperationsDocument into Document Bugfix: jsonapi/version was missing in error responses * Fill error.source.header where applicable * Breaking: Renamed "total-resources" meta key to "total" because thats what Ember.js expects it to be named (see https://guides.emberjs.com/release/models/handling-metadata/) * Removed unneeded StringEnumConverter usage. Also removed it from the defaults for tests, because that hides the problem when we forget to put it on a member that needs it. * Use configured STJ options for null/default value inclusion Bugfix: do not break out of method on first attribute * Fixed data type in json request body * Added missing type, which is a required element * Converted core code to use System.Text.Json - Added various converters to steer JsonSerializer in the right direction - JsonApiDotNetCore.Serialization.Objects - Removed inheritance in JsonApiDotNetCore.Serialization.Objects, so we're in control of element write order - Moved "meta" to the end in all types (it is secondary information) - Consistently set IgnoreCondition on all properties, so we don't need to override global options anymore * Updated documentation * Fixed broken example-generation. Set launchBrowser to true, so it shows sample data on F5. * Inlined properties on serializable objects * Add test for incompatible ID value. By default, this produces: ``` The JSON value could not be converted to JsonApiDotNetCore.Serialization.Objects.SingleOrManyData`1[JsonApiDotNetCore.Serialization.Objects.ResourceObject]. Path: $.data | LineNumber: 3 | BytePositionInLine: 11. ``` which is totally unhelpful. Because this is so likely to hit users, we special-case here to produce a better error. * Removed misplaced launchsettings.json * Review feedback: use base class instead of static helper --- Directory.Build.props | 9 +- benchmarks/DependencyFactory.cs | 3 + benchmarks/Query/QueryParserBenchmarks.cs | 4 +- .../JsonApiDeserializerBenchmarks.cs | 18 +- .../JsonApiSerializerBenchmarks.cs | 2 +- docs/request-examples/012_PATCH_Book.ps1 | 2 +- docs/usage/options.md | 21 +- docs/usage/resource-graph.md | 2 +- docs/usage/resources/attributes.md | 15 +- docs/usage/resources/relationships.md | 2 +- docs/usage/routing.md | 2 +- .../Properties/launchSettings.json | 4 +- src/Examples/GettingStarted/Startup.cs | 3 +- .../Controllers/NonJsonApiController.cs | 6 +- .../JsonApiDotNetCoreExample/Startup.cs | 7 +- .../AtomicOperations/LocalIdTracker.cs | 8 +- .../AtomicOperations/LocalIdValidator.cs | 20 +- .../OperationProcessorAccessor.cs | 10 +- .../AtomicOperations/OperationsProcessor.cs | 23 +- .../Processors/CreateProcessor.cs | 10 +- .../Configuration/IJsonApiOptions.cs | 51 +- .../Configuration/IResourceContextProvider.cs | 33 -- .../Configuration/IResourceGraph.cs | 46 +- .../InverseNavigationResolver.cs | 10 +- .../JsonApiApplicationBuilder.cs | 10 +- .../Configuration/JsonApiOptions.cs | 53 +- .../Configuration/JsonApiValidationFilter.cs | 2 +- .../Configuration/ResourceContext.cs | 82 ++- .../Configuration/ResourceGraph.cs | 48 +- .../Configuration/ResourceGraphBuilder.cs | 6 +- .../Configuration/ResourceNameFormatter.cs | 18 +- .../ServiceCollectionExtensions.cs | 21 - .../Configuration/TypeLocator.cs | 4 +- .../Controllers/BaseJsonApiController.cs | 4 +- .../BaseJsonApiOperationsController.cs | 2 +- .../Controllers/CoreJsonApiController.cs | 10 +- ...annotClearRequiredRelationshipException.cs | 2 +- .../Errors/InvalidModelStateException.cs | 46 +- .../Errors/InvalidQueryException.cs | 2 +- .../InvalidQueryStringParameterException.cs | 4 +- .../Errors/InvalidRequestBodyException.cs | 4 +- .../Errors/JsonApiException.cs | 21 +- .../MissingTransactionSupportException.cs | 4 +- .../NonParticipatingTransactionException.cs | 4 +- .../Errors/RelationshipNotFoundException.cs | 2 +- .../RequestMethodNotAllowedException.cs | 2 +- .../Errors/ResourceAlreadyExistsException.cs | 2 +- ...ceIdInCreateResourceNotAllowedException.cs | 4 +- .../Errors/ResourceIdMismatchException.cs | 4 +- .../Errors/ResourceNotFoundException.cs | 2 +- .../Errors/ResourceTypeMismatchException.cs | 6 +- ...sourcesInRelationshipsNotFoundException.cs | 2 +- .../ToManyRelationshipRequiredException.cs | 2 +- .../UnsuccessfulActionResultException.cs | 7 +- .../JsonApiDotNetCore.csproj | 1 - .../Middleware/AsyncJsonApiExceptionFilter.cs | 6 +- .../Middleware/ExceptionHandler.cs | 28 +- .../Middleware/IExceptionHandler.cs | 4 +- .../Middleware/JsonApiMiddleware.cs | 85 ++- .../Middleware/JsonApiRoutingConvention.cs | 17 +- .../Middleware/TraceLogWriter.cs | 23 +- .../Properties/launchSettings.json | 27 - .../Queries/Internal/Parsing/FilterParser.cs | 12 +- .../Queries/Internal/Parsing/IncludeParser.cs | 5 +- .../Internal/Parsing/PaginationParser.cs | 5 +- .../Internal/Parsing/QueryExpressionParser.cs | 8 +- .../QueryStringParameterScopeParser.cs | 4 +- .../Parsing/ResourceFieldChainResolver.cs | 22 +- .../Queries/Internal/Parsing/SortParser.cs | 5 +- .../Internal/Parsing/SparseFieldSetParser.cs | 5 +- .../Internal/Parsing/SparseFieldTypeParser.cs | 10 +- .../Queries/Internal/QueryLayerComposer.cs | 18 +- .../QueryableBuilding/IncludeClauseBuilder.cs | 15 +- .../QueryableBuilding/QueryableBuilder.cs | 13 +- .../QueryableBuilding/SelectClauseBuilder.cs | 12 +- .../Queries/Internal/SparseFieldSetCache.cs | 7 +- src/JsonApiDotNetCore/Queries/QueryLayer.cs | 4 +- .../IDefaultsQueryStringParameterReader.cs | 15 - .../INullsQueryStringParameterReader.cs | 15 - .../DefaultsQueryStringParameterReader.cs | 54 -- .../FilterQueryStringParameterReader.cs | 8 +- .../IncludeQueryStringParameterReader.cs | 6 +- .../Internal/LegacyFilterNotationConverter.cs | 4 +- .../NullsQueryStringParameterReader.cs | 54 -- .../PaginationQueryStringParameterReader.cs | 6 +- .../Internal/QueryStringParameterReader.cs | 10 +- .../Internal/QueryStringReader.cs | 4 +- .../SortQueryStringParameterReader.cs | 8 +- ...parseFieldSetQueryStringParameterReader.cs | 10 +- .../JsonApiQueryStringParameters.cs | 4 +- .../ResourceRepositoryAccessor.cs | 14 +- .../Internal/RuntimeTypeConverter.cs | 8 +- .../Resources/ResourceChangeTracker.cs | 19 +- .../Resources/ResourceDefinitionAccessor.cs | 10 +- .../AtomicOperationsResponseSerializer.cs | 24 +- .../Serialization/BaseDeserializer.cs | 134 +++-- .../Serialization/BaseSerializer.cs | 32 +- .../IResourceObjectBuilderSettingsProvider.cs | 13 - .../Building/IncludedResourceObjectBuilder.cs | 46 +- .../Serialization/Building/LinkBuilder.cs | 17 +- .../Serialization/Building/MetaBuilder.cs | 6 +- ...omparer.cs => ResourceIdentityComparer.cs} | 10 +- .../Building/ResourceObjectBuilder.cs | 60 +-- .../Building/ResourceObjectBuilderSettings.cs | 23 - .../ResourceObjectBuilderSettingsProvider.cs | 30 -- .../Building/ResponseResourceObjectBuilder.cs | 36 +- .../Internal/DeserializedResponseBase.cs | 18 - .../Client/Internal/IRequestSerializer.cs | 43 -- .../Client/Internal/IResponseDeserializer.cs | 37 -- .../Client/Internal/ManyResponse.cs | 19 - .../Client/Internal/RequestSerializer.cs | 103 ---- .../Client/Internal/ResponseDeserializer.cs | 149 ------ .../Client/Internal/SingleResponse.cs | 18 - .../Serialization/ETagGenerator.cs | 2 +- .../Serialization/FieldsToSerialize.cs | 12 +- .../Serialization/IJsonApiDeserializer.cs | 3 +- .../Serialization/JsonApiReader.cs | 18 +- .../Serialization/JsonApiWriter.cs | 21 +- .../JsonConverters/ResourceObjectConverter.cs | 266 ++++++++++ .../SingleOrManyDataConverterFactory.cs | 74 +++ .../WriteOnlyDocumentConverter.cs | 86 ++++ .../WriteOnlyRelationshipObjectConverter.cs | 51 ++ .../Serialization/JsonInvalidAttributeInfo.cs | 29 ++ .../Serialization/JsonObjectConverter.cs | 35 ++ .../Serialization/JsonSerializerExtensions.cs | 55 -- .../Objects/AtomicOperationCode.cs | 7 +- .../Objects/AtomicOperationObject.cs | 26 +- .../Objects/AtomicOperationsDocument.cs | 41 -- .../Serialization/Objects/AtomicReference.cs | 30 +- .../Objects/AtomicResultObject.cs | 15 +- .../Serialization/Objects/Document.cs | 71 ++- .../Serialization/Objects/Error.cs | 92 ---- .../Serialization/Objects/ErrorDocument.cs | 44 -- .../Serialization/Objects/ErrorLinks.cs | 17 +- .../Serialization/Objects/ErrorMeta.cs | 29 -- .../Serialization/Objects/ErrorObject.cs | 59 +++ .../Serialization/Objects/ErrorSource.cs | 24 +- .../Serialization/Objects/ExposableData.cs | 113 ---- .../Objects/IResourceIdentity.cs | 9 + .../Serialization/Objects/JsonApiObject.cs | 25 +- .../Objects/RelationshipEntry.cs | 14 - .../Objects/RelationshipLinks.cs | 19 +- .../Objects/RelationshipObject.cs | 25 + .../Objects/ResourceIdentifierObject.cs | 55 +- .../Serialization/Objects/ResourceLinks.cs | 13 +- .../Serialization/Objects/ResourceObject.cs | 35 +- .../Serialization/Objects/SingleOrManyData.cs | 47 ++ .../Serialization/Objects/TopLevelLinks.cs | 67 +-- .../Serialization/RequestDeserializer.cs | 76 ++- .../Serialization/ResponseSerializer.cs | 39 +- src/JsonApiDotNetCore/TypeExtensions.cs | 25 + .../ServiceDiscoveryFacadeTests.cs | 7 +- .../Archiving/ArchiveTests.cs | 122 ++--- .../TelevisionBroadcastDefinition.cs | 8 +- ...micConstrainedOperationsControllerTests.cs | 15 +- .../CreateMusicTrackOperationsController.cs | 4 +- .../Creating/AtomicCreateResourceTests.cs | 139 +++-- ...reateResourceWithClientGeneratedIdTests.cs | 33 +- ...eateResourceWithToManyRelationshipTests.cs | 90 ++-- ...reateResourceWithToOneRelationshipTests.cs | 96 ++-- .../Deleting/AtomicDeleteResourceTests.cs | 50 +- ...mplicitlyChangingTextLanguageDefinition.cs | 2 +- .../Links/AtomicAbsoluteLinksTests.cs | 24 +- .../AtomicRelativeLinksWithNamespaceTests.cs | 35 +- .../LocalIds/AtomicLocalIdTests.cs | 463 ++++++++--------- .../Meta/AtomicResourceMetaTests.cs | 19 +- .../Meta/AtomicResponseMetaTests.cs | 21 +- .../Mixed/AtomicRequestBodyTests.cs | 28 +- .../Mixed/AtomicSerializationTests.cs | 74 ++- .../Mixed/MaximumOperationsPerRequestTests.cs | 13 +- .../AtomicModelStateValidationTests.cs | 27 +- .../QueryStrings/AtomicQueryStringTests.cs | 124 +---- ...micSerializationResourceDefinitionTests.cs | 32 +- ...icSparseFieldSetResourceDefinitionTests.cs | 22 +- .../Transactions/AtomicRollbackTests.cs | 10 +- .../AtomicTransactionConsistencyTests.cs | 12 +- .../AtomicAddToToManyRelationshipTests.cs | 124 ++--- ...AtomicRemoveFromToManyRelationshipTests.cs | 116 +++-- .../AtomicReplaceToManyRelationshipTests.cs | 122 ++--- .../AtomicUpdateToOneRelationshipTests.cs | 114 +++-- .../AtomicReplaceToManyRelationshipTests.cs | 62 +-- .../Resources/AtomicUpdateResourceTests.cs | 175 ++++--- .../AtomicUpdateToOneRelationshipTests.cs | 56 +- .../CompositeKeys/CarExpressionRewriter.cs | 8 +- .../CompositeKeys/CompositeKeyTests.cs | 28 +- .../ContentNegotiation/AcceptHeaderTests.cs | 36 +- .../ContentTypeHeaderTests.cs | 54 +- .../ActionResultTests.cs | 40 +- .../BaseToothbrushesController.cs | 10 +- .../ApiControllerAttributeTests.cs | 4 +- .../CustomRoutes/CustomRouteTests.cs | 30 +- .../EagerLoading/EagerLoadingTests.cs | 70 +-- .../AlternateExceptionHandler.cs | 9 +- ...umerArticleIsNoLongerAvailableException.cs | 2 +- .../ExceptionHandlerTests.cs | 20 +- .../HostingInIIS/HostingTests.cs | 46 +- .../IdObfuscation/HexadecimalCodec.cs | 4 +- .../IdObfuscation/IdObfuscationTests.cs | 54 +- .../ModelState/ModelStateValidationTests.cs | 68 +-- .../ModelState/NoModelStateValidationTests.cs | 8 +- .../RequestBody/WorkflowDefinition.cs | 8 +- .../RequestBody/WorkflowTests.cs | 14 +- .../Links/AbsoluteLinksWithNamespaceTests.cs | 116 ++--- .../AbsoluteLinksWithoutNamespaceTests.cs | 116 ++--- .../Links/LinkInclusionTests.cs | 20 +- .../Links/RelativeLinksWithNamespaceTests.cs | 74 +-- .../RelativeLinksWithoutNamespaceTests.cs | 74 +-- .../Meta/ResourceMetaTests.cs | 16 +- .../Meta/ResponseMetaTests.cs | 12 +- .../Meta/TopLevelCountTests.cs | 7 +- .../FireForgetTests.Group.cs | 20 +- .../FireForgetTests.User.cs | 28 +- .../FireAndForgetDelivery/FireForgetTests.cs | 16 +- .../FireAndForgetDelivery/MessageBroker.cs | 2 +- .../Microservices/Messages/OutgoingMessage.cs | 8 +- .../OutboxTests.Group.cs | 20 +- .../OutboxTests.User.cs | 28 +- .../TransactionalOutboxPattern/OutboxTests.cs | 10 +- .../MultiTenancy/MultiTenancyTests.cs | 120 ++--- .../JsonKebabCaseNamingPolicy.cs | 83 +++ .../KebabCasingConventionStartup.cs | 7 +- .../NamingConventions/KebabCasingTests.cs | 53 +- .../PascalCasingConventionStartup.cs | 25 + .../NamingConventions/PascalCasingTests.cs | 205 ++++++++ .../IntegrationTests/QueryStrings/Calendar.cs | 3 + .../Filtering/FilterDataTypeTests.cs | 46 +- .../Filtering/FilterDepthTests.cs | 62 +-- .../Filtering/FilterOperatorTests.cs | 76 +-- .../QueryStrings/Filtering/FilterTests.cs | 30 +- .../QueryStrings/Includes/IncludeTests.cs | 121 ++--- .../QueryStrings/LabelColor.cs | 2 + .../PaginationWithTotalCountTests.cs | 120 ++--- .../PaginationWithoutTotalCountTests.cs | 40 +- .../Pagination/RangeValidationTests.cs | 14 +- .../RangeValidationWithMaximumTests.cs | 28 +- .../QueryStrings/QueryStringTests.cs | 12 +- .../SerializerDefaultValueHandlingTests.cs | 102 ---- .../SerializerIgnoreConditionTests.cs | 85 +++ .../SerializerNullValueHandlingTests.cs | 102 ---- .../QueryStrings/Sorting/SortTests.cs | 108 ++-- .../SparseFieldSets/SparseFieldSetTests.cs | 229 ++++----- .../ReadWrite/Creating/CreateResourceTests.cs | 138 ++--- ...reateResourceWithClientGeneratedIdTests.cs | 30 +- ...eateResourceWithToManyRelationshipTests.cs | 105 ++-- ...reateResourceWithToOneRelationshipTests.cs | 84 +-- .../ReadWrite/Deleting/DeleteResourceTests.cs | 22 +- .../Fetching/FetchRelationshipTests.cs | 48 +- .../ReadWrite/Fetching/FetchResourceTests.cs | 103 ++-- .../ImplicitlyChangingWorkItemDefinition.cs | 2 +- ...plicitlyChangingWorkItemGroupDefinition.cs | 2 +- .../AddToToManyRelationshipTests.cs | 96 ++-- .../RemoveFromToManyRelationshipTests.cs | 94 ++-- .../ReplaceToManyRelationshipTests.cs | 92 ++-- .../UpdateToOneRelationshipTests.cs | 68 +-- .../ReplaceToManyRelationshipTests.cs | 142 ++--- .../Updating/Resources/UpdateResourceTests.cs | 254 +++++---- .../Resources/UpdateToOneRelationshipTests.cs | 104 ++-- .../ReadWrite/WorkItemPriority.cs | 2 + .../DefaultBehaviorTests.cs | 58 +-- .../ResourceInjectionTests.cs | 51 +- .../Reading/MoonDefinition.cs | 3 +- .../Reading/PlanetDefinition.cs | 4 +- .../Reading/ResourceDefinitionReadTests.cs | 99 ++-- .../ResourceDefinitions/Reading/StarKind.cs | 2 + .../ResourceDefinitionSerializationTests.cs | 52 +- .../ResourceInheritance/InheritanceTests.cs | 26 +- .../DisableQueryStringTests.cs | 12 +- .../HttpReadOnlyTests.cs | 16 +- .../NoHttpDeleteTests.cs | 8 +- .../RestrictedControllers/NoHttpPatchTests.cs | 8 +- .../RestrictedControllers/NoHttpPostTests.cs | 8 +- .../Serialization/ETagTests.cs | 13 +- .../Serialization/MeetingLocation.cs | 8 +- .../Serialization/SerializationTests.cs | 119 ++++- .../SoftDeletion/SoftDeletionTests.cs | 114 ++--- .../ZeroKeys/EmptyGuidAsKeyTests.cs | 18 +- .../ZeroKeys/ZeroAsKeyTests.cs | 32 +- .../JsonApiDotNetCoreTests.csproj | 1 + .../DefaultsParseTests.cs | 135 ----- .../QueryStringParameters/NullsParseTests.cs | 134 ----- test/MultiDbContextTests/ResourceTests.cs | 20 +- test/NoEntityFrameworkTests/WorkItemTests.cs | 31 +- .../TestBuildingBlocks/DbContextExtensions.cs | 4 +- test/TestBuildingBlocks/FakerContainer.cs | 2 +- .../HttpResponseMessageExtensions.cs | 30 +- test/TestBuildingBlocks/IntegrationTest.cs | 9 +- .../IntegrationTestConfiguration.cs | 19 - .../IntegrationTestContext.cs | 11 + .../JsonApiStringConverter.cs | 25 + .../ObjectAssertionsExtensions.cs | 61 ++- test/TestBuildingBlocks/TestableStartup.cs | 5 +- test/TestBuildingBlocks/Unknown.cs | 87 ++++ .../Builders/ResourceGraphBuilderTests.cs | 22 +- .../Controllers/BaseJsonApiControllerTests.cs | 26 +- .../ServiceCollectionExtensionsTests.cs | 15 +- test/UnitTests/Internal/ErrorDocumentTests.cs | 5 +- .../RequestScopedServiceProviderTests.cs | 2 +- .../Internal/ResourceGraphBuilderTests.cs | 8 +- .../Middleware/JsonApiMiddlewareTests.cs | 19 +- .../Middleware/JsonApiRequestTests.cs | 1 + .../UnitTests/Models/RelationshipDataTests.cs | 105 ---- .../Models/ResourceConstructionTests.cs | 26 +- .../Client/RequestSerializerTests.cs | 298 ----------- .../Client/ResponseDeserializerTests.cs | 483 ------------------ .../Common/BaseDocumentBuilderTests.cs | 14 +- .../Common/BaseDocumentParserTests.cs | 88 ++-- .../Common/ResourceObjectBuilderTests.cs | 28 +- .../Serialization/DeserializerTestsSetup.cs | 44 +- .../Serialization/SerializerTestsSetup.cs | 24 +- .../IncludedResourceObjectBuilderTests.cs | 13 +- .../Server/RequestDeserializerTests.cs | 33 +- .../ResponseResourceObjectBuilderTests.cs | 32 +- .../Server/ResponseSerializerTests.cs | 19 +- 313 files changed, 5941 insertions(+), 6730 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs delete mode 100644 src/JsonApiDotNetCore/Properties/launchSettings.json delete mode 100644 src/JsonApiDotNetCore/QueryStrings/IDefaultsQueryStringParameterReader.cs delete mode 100644 src/JsonApiDotNetCore/QueryStrings/INullsQueryStringParameterReader.cs delete mode 100644 src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs delete mode 100644 src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs rename src/JsonApiDotNetCore/Serialization/Building/{ResourceIdentifierObjectComparer.cs => ResourceIdentityComparer.cs} (60%) delete mode 100644 src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettings.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs create mode 100644 src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs create mode 100644 src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyDocumentConverter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyRelationshipObjectConverter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/JsonInvalidAttributeInfo.cs create mode 100644 src/JsonApiDotNetCore/Serialization/JsonObjectConverter.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/JsonSerializerExtensions.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Objects/Error.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs create mode 100644 src/JsonApiDotNetCore/Serialization/Objects/ErrorObject.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs create mode 100644 src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Objects/RelationshipEntry.cs create mode 100644 src/JsonApiDotNetCore/Serialization/Objects/RelationshipObject.cs create mode 100644 src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs create mode 100644 test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/JsonKebabCaseNamingPolicy.cs create mode 100644 test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingConventionStartup.cs create mode 100644 test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs delete mode 100644 test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerDefaultValueHandlingTests.cs create mode 100644 test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerIgnoreConditionTests.cs delete mode 100644 test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerNullValueHandlingTests.cs delete mode 100644 test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/DefaultsParseTests.cs delete mode 100644 test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/NullsParseTests.cs delete mode 100644 test/TestBuildingBlocks/IntegrationTestConfiguration.cs create mode 100644 test/TestBuildingBlocks/JsonApiStringConverter.cs create mode 100644 test/TestBuildingBlocks/Unknown.cs delete mode 100644 test/UnitTests/Models/RelationshipDataTests.cs delete mode 100644 test/UnitTests/Serialization/Client/RequestSerializerTests.cs delete mode 100644 test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs diff --git a/Directory.Build.props b/Directory.Build.props index dbd5c5ab26..bec8ba912e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,11 +5,12 @@ 5.0.* 5.0.* $(MSBuildThisFileDirectory)CodingGuidelines.ruleset + 9999 - + @@ -21,11 +22,11 @@ - 33.0.2 + 33.1.1 3.1.0 - 5.10.3 + 6.1.0 4.16.1 2.4.* - 16.10.0 + 16.11.0 diff --git a/benchmarks/DependencyFactory.cs b/benchmarks/DependencyFactory.cs index 7ecbafffbc..184ba5a082 100644 --- a/benchmarks/DependencyFactory.cs +++ b/benchmarks/DependencyFactory.cs @@ -8,7 +8,10 @@ internal sealed class DependencyFactory public IResourceGraph CreateResourceGraph(IJsonApiOptions options) { var builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); + builder.Add(BenchmarkResourcePublicNames.Type); + builder.Add(); + return builder.Build(); } } diff --git a/benchmarks/Query/QueryParserBenchmarks.cs b/benchmarks/Query/QueryParserBenchmarks.cs index fd791f5d8a..8f1ec950da 100644 --- a/benchmarks/Query/QueryParserBenchmarks.cs +++ b/benchmarks/Query/QueryParserBenchmarks.cs @@ -64,11 +64,9 @@ private static QueryStringReader CreateQueryParameterDiscoveryForAll(IResourceGr var sortReader = new SortQueryStringParameterReader(request, resourceGraph); var sparseFieldSetReader = new SparseFieldSetQueryStringParameterReader(request, resourceGraph); var paginationReader = new PaginationQueryStringParameterReader(request, resourceGraph, options); - var defaultsReader = new DefaultsQueryStringParameterReader(options); - var nullsReader = new NullsQueryStringParameterReader(options); IQueryStringParameterReader[] readers = ArrayFactory.Create(includeReader, filterReader, sortReader, - sparseFieldSetReader, paginationReader, defaultsReader, nullsReader); + sparseFieldSetReader, paginationReader); return new QueryStringReader(options, queryStringAccessor, readers, NullLoggerFactory.Instance); } diff --git a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs index 6e3bcf2b61..2c2cb62223 100644 --- a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.Design; +using System.Text.Json; using BenchmarkDotNet.Attributes; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; namespace Benchmarks.Serialization { @@ -16,15 +13,14 @@ namespace Benchmarks.Serialization [MarkdownExporter] public class JsonApiDeserializerBenchmarks { - private static readonly string Content = JsonConvert.SerializeObject(new Document + private static readonly string RequestBody = JsonSerializer.Serialize(new { - Data = new ResourceObject + data = new { - Type = BenchmarkResourcePublicNames.Type, - Id = "1", - Attributes = new Dictionary + type = BenchmarkResourcePublicNames.Type, + id = "1", + attributes = new { - ["name"] = Guid.NewGuid().ToString() } } }); @@ -55,7 +51,7 @@ public JsonApiDeserializerBenchmarks() [Benchmark] public object DeserializeSimpleObject() { - return _jsonApiDeserializer.Deserialize(Content); + return _jsonApiDeserializer.Deserialize(RequestBody); } } } diff --git a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs index ffbebc2cfc..0fa58c272e 100644 --- a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs @@ -34,7 +34,7 @@ public JsonApiSerializerBenchmarks() ILinkBuilder linkBuilder = new Mock().Object; IIncludedResourceObjectBuilder includeBuilder = new Mock().Object; - var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings()); + var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, options); IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock().Object; diff --git a/docs/request-examples/012_PATCH_Book.ps1 b/docs/request-examples/012_PATCH_Book.ps1 index 080115161c..d704c8c8c8 100644 --- a/docs/request-examples/012_PATCH_Book.ps1 +++ b/docs/request-examples/012_PATCH_Book.ps1 @@ -4,7 +4,7 @@ curl -s -f http://localhost:14141/api/books/1 ` -d '{ \"data\": { \"type\": \"books\", - \"id\": "1", + \"id\": \"1\", \"attributes\": { \"publishYear\": 1820 } diff --git a/docs/usage/options.md b/docs/usage/options.md index 287c1c52f1..e2e099e31e 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -78,27 +78,26 @@ To limit the maximum depth of nested includes, use `MaximumIncludeDepth`. This i options.MaximumIncludeDepth = 1; ``` -## Custom Serializer Settings +## Customize Serializer options -We use [Newtonsoft.Json](https://www.newtonsoft.com/json) for all serialization needs. -If you want to change the default serializer settings, you can: +We use [System.Text.Json](https://www.nuget.org/packages/System.Text.Json) for all serialization needs. +If you want to change the default serializer options, you can: ```c# -options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; -options.SerializerSettings.Converters.Add(new StringEnumConverter()); -options.SerializerSettings.Formatting = Formatting.Indented; +options.SerializerOptions.WriteIndented = true; +options.SerializerOptions.ReferenceHandler = ReferenceHandler.Preserve; +options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); ``` The default naming convention (as used in the routes and resource/attribute/relationship names) is also determined here, and can be changed (default is camel-case): ```c# -options.SerializerSettings.ContractResolver = new DefaultContractResolver -{ - NamingStrategy = new KebabCaseNamingStrategy() -}; +// Use Pascal case +options.SerializerOptions.PropertyNamingPolicy = null; +options.SerializerOptions.DictionaryKeyPolicy = null; ``` -Because we copy resource properties into an intermediate object before serialization, Newtonsoft.Json annotations on properties are ignored. +Because we copy resource properties into an intermediate object before serialization, JSON annotations such as `[JsonPropertyName]` and `[JsonIgnore]` on `[Attr]` properties are ignored. ## Enable ModelState Validation diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index c77c842429..36e424d6e0 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -98,4 +98,4 @@ public class MyModel : Identifiable } ``` -The default naming convention can be changed in [options](~/usage/options.md#custom-serializer-settings). +The default naming convention can be changed in [options](~/usage/options.md#customize-serializer-options). diff --git a/docs/usage/resources/attributes.md b/docs/usage/resources/attributes.md index 6e24ab964f..6a42bae7e0 100644 --- a/docs/usage/resources/attributes.md +++ b/docs/usage/resources/attributes.md @@ -14,7 +14,7 @@ public class Person : Identifiable There are two ways the exposed attribute name is determined: -1. Using the configured [naming convention](~/usage/options.md#custom-serializer-settings). +1. Using the configured [naming convention](~/usage/options.md#customize-serializer-options). 2. Individually using the attribute's constructor. ```c# @@ -88,9 +88,9 @@ public class Person : Identifiable ## Complex Attributes Models may contain complex attributes. -Serialization of these types is done by [Newtonsoft.Json](https://www.newtonsoft.com/json), -so you should use their APIs to specify serialization formats. -You can also use global options to specify `JsonSerializer` configuration. +Serialization of these types is done by [System.Text.Json](https://www.nuget.org/packages/System.Text.Json), +so you should use their APIs to specify serialization format. +You can also use [global options](~/usage/options.md#customize-serializer-options) to control the `JsonSerializer` behavior. ```c# public class Foo : Identifiable @@ -101,7 +101,8 @@ public class Foo : Identifiable public class Bar { - [JsonProperty("compound-member")] + [JsonPropertyName("compound-member")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string CompoundMember { get; set; } } ``` @@ -121,13 +122,13 @@ public class Foo : Identifiable { get { - return Bar == null ? "{}" : JsonConvert.SerializeObject(Bar); + return Bar == null ? "{}" : JsonSerializer.Serialize(Bar); } set { Bar = string.IsNullOrWhiteSpace(value) ? null - : JsonConvert.DeserializeObject(value); + : JsonSerializer.Deserialize(value); } } } diff --git a/docs/usage/resources/relationships.md b/docs/usage/resources/relationships.md index 172a8788ec..2495419a6a 100644 --- a/docs/usage/resources/relationships.md +++ b/docs/usage/resources/relationships.md @@ -64,7 +64,7 @@ The left side of this relationship is of type `Article` (public name: "articles" There are two ways the exposed relationship name is determined: -1. Using the configured [naming convention](~/usage/options.md#custom-serializer-settings). +1. Using the configured [naming convention](~/usage/options.md#customize-serializer-options). 2. Individually using the attribute's constructor. ```c# diff --git a/docs/usage/routing.md b/docs/usage/routing.md index 314e2bdfb1..0a10831d9b 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -45,7 +45,7 @@ The exposed name of the resource ([which can be customized](~/usage/resource-gra ### Non-JSON:API controllers -If a controller does not inherit from `JsonApiController`, the [configured naming convention](~/usage/options.md#custom-serializer-settings) is applied to the name of the controller. +If a controller does not inherit from `JsonApiController`, the [configured naming convention](~/usage/options.md#customize-serializer-options) is applied to the name of the controller. ```c# public class OrderLineController : ControllerBase diff --git a/src/Examples/GettingStarted/Properties/launchSettings.json b/src/Examples/GettingStarted/Properties/launchSettings.json index b68c2481ed..bcf154605c 100644 --- a/src/Examples/GettingStarted/Properties/launchSettings.json +++ b/src/Examples/GettingStarted/Properties/launchSettings.json @@ -10,7 +10,7 @@ "profiles": { "IIS Express": { "commandName": "IISExpress", - "launchBrowser": false, + "launchBrowser": true, "launchUrl": "api/people", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -18,7 +18,7 @@ }, "Kestrel": { "commandName": "Project", - "launchBrowser": false, + "launchBrowser": true, "launchUrl": "api/people", "applicationUrl": "http://localhost:14141", "environmentVariables": { diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs index b942ecc98d..10d2f338f0 100644 --- a/src/Examples/GettingStarted/Startup.cs +++ b/src/Examples/GettingStarted/Startup.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; namespace GettingStarted { @@ -21,7 +20,7 @@ public void ConfigureServices(IServiceCollection services) options.Namespace = "api"; options.UseRelativeLinks = true; options.IncludeTotalResourceCount = true; - options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerOptions.WriteIndented = true; }); } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs index 49708c5465..b26b82d27d 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs @@ -28,21 +28,21 @@ public async Task PostAsync() return BadRequest("Please send your name."); } - string result = "Hello, " + name; + string result = $"Hello, {name}"; return Ok(result); } [HttpPut] public IActionResult Put([FromBody] string name) { - string result = "Hi, " + name; + string result = $"Hi, {name}"; return Ok(result); } [HttpPatch] public IActionResult Patch(string name) { - string result = "Good day, " + name; + string result = $"Good day, {name}"; return Ok(result); } diff --git a/src/Examples/JsonApiDotNetCoreExample/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startup.cs index 85740b83e1..fb75cfaef8 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startup.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json.Serialization; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCoreExample.Data; @@ -9,8 +10,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace JsonApiDotNetCoreExample { @@ -52,8 +51,8 @@ public void ConfigureServices(IServiceCollection services) options.UseRelativeLinks = true; options.ValidateModelState = true; options.IncludeTotalResourceCount = true; - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.Converters.Add(new StringEnumConverter()); + options.SerializerOptions.WriteIndented = true; + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); #if DEBUG options.IncludeExceptionStackTraceInErrors = true; #endif diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs index 453267d828..744a03a9e8 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs @@ -32,7 +32,7 @@ private void AssertIsNotDeclared(string localId) { if (_idsTracked.ContainsKey(localId)) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Another local ID with the same name is already defined at this point.", Detail = $"Another local ID with name '{localId}' is already defined at this point." @@ -75,7 +75,7 @@ public string GetValue(string localId, string resourceType) if (item.ServerId == null) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Local ID cannot be both defined and used within the same operation.", Detail = $"Local ID '{localId}' cannot be both defined and used within the same operation." @@ -89,7 +89,7 @@ private void AssertIsDeclared(string localId) { if (!_idsTracked.ContainsKey(localId)) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Server-generated value for local ID is not available at this point.", Detail = $"Server-generated value for local ID '{localId}' is not available at this point." @@ -101,7 +101,7 @@ private static void AssertSameResourceType(string currentType, string declaredTy { if (declaredType != currentType) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Type mismatch in local ID usage.", Detail = $"Local ID '{localId}' belongs to resource type '{declaredType}' instead of '{currentType}'." diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs index 6bd8e7a57e..d880ab7b42 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs @@ -15,15 +15,15 @@ namespace JsonApiDotNetCore.AtomicOperations public sealed class LocalIdValidator { private readonly ILocalIdTracker _localIdTracker; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public LocalIdValidator(ILocalIdTracker localIdTracker, IResourceContextProvider resourceContextProvider) + public LocalIdValidator(ILocalIdTracker localIdTracker, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _localIdTracker = localIdTracker; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } public void Validate(IEnumerable operations) @@ -45,9 +45,10 @@ public void Validate(IEnumerable operations) } catch (JsonApiException exception) { - foreach (Error error in exception.Errors) + foreach (ErrorObject error in exception.Errors) { - error.Source.Pointer = $"/atomic:operations[{operationIndex}]" + error.Source.Pointer; + error.Source ??= new ErrorSource(); + error.Source.Pointer = $"/atomic:operations[{operationIndex}]{error.Source.Pointer}"; } throw; @@ -80,7 +81,7 @@ private void DeclareLocalId(IIdentifiable resource) { if (resource.LocalId != null) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resource.GetType()); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resource.GetType()); _localIdTracker.Declare(resource.LocalId, resourceContext.PublicName); } } @@ -89,8 +90,7 @@ private void AssignLocalId(OperationContainer operation) { if (operation.Resource.LocalId != null) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(operation.Resource.GetType()); - + ResourceContext resourceContext = _resourceGraph.GetResourceContext(operation.Resource.GetType()); _localIdTracker.Assign(operation.Resource.LocalId, resourceContext.PublicName, "placeholder"); } } @@ -99,7 +99,7 @@ private void AssertLocalIdIsAssigned(IIdentifiable resource) { if (resource.LocalId != null) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resource.GetType()); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resource.GetType()); _localIdTracker.GetValue(resource.LocalId, resourceContext.PublicName); } } diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs index b6d7bae21d..a71fa906cd 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs @@ -14,15 +14,15 @@ namespace JsonApiDotNetCore.AtomicOperations [PublicAPI] public class OperationProcessorAccessor : IOperationProcessorAccessor { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IServiceProvider _serviceProvider; - public OperationProcessorAccessor(IResourceContextProvider resourceContextProvider, IServiceProvider serviceProvider) + public OperationProcessorAccessor(IResourceGraph resourceGraph, IServiceProvider serviceProvider) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _serviceProvider = serviceProvider; } @@ -38,7 +38,7 @@ public Task ProcessAsync(OperationContainer operation, Cance protected virtual IOperationProcessor ResolveProcessor(OperationContainer operation) { Type processorInterface = GetProcessorInterface(operation.Kind); - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(operation.Resource.GetType()); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(operation.Resource.GetType()); Type processorType = processorInterface.MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); return (IOperationProcessor)_serviceProvider.GetRequiredService(processorType); diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs index ffb2e19efc..8d531ea231 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs @@ -19,28 +19,28 @@ public class OperationsProcessor : IOperationsProcessor private readonly IOperationProcessorAccessor _operationProcessorAccessor; private readonly IOperationsTransactionFactory _operationsTransactionFactory; private readonly ILocalIdTracker _localIdTracker; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IJsonApiRequest _request; private readonly ITargetedFields _targetedFields; private readonly LocalIdValidator _localIdValidator; public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccessor, IOperationsTransactionFactory operationsTransactionFactory, - ILocalIdTracker localIdTracker, IResourceContextProvider resourceContextProvider, IJsonApiRequest request, ITargetedFields targetedFields) + ILocalIdTracker localIdTracker, IResourceGraph resourceGraph, IJsonApiRequest request, ITargetedFields targetedFields) { ArgumentGuard.NotNull(operationProcessorAccessor, nameof(operationProcessorAccessor)); ArgumentGuard.NotNull(operationsTransactionFactory, nameof(operationsTransactionFactory)); ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); _operationProcessorAccessor = operationProcessorAccessor; _operationsTransactionFactory = operationsTransactionFactory; _localIdTracker = localIdTracker; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _request = request; _targetedFields = targetedFields; - _localIdValidator = new LocalIdValidator(_localIdTracker, _resourceContextProvider); + _localIdValidator = new LocalIdValidator(_localIdTracker, _resourceGraph); } /// @@ -77,9 +77,10 @@ public virtual async Task> ProcessAsync(IList> ProcessAsync(IList : ICreateProcessor { private readonly ICreateService _service; private readonly ILocalIdTracker _localIdTracker; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public CreateProcessor(ICreateService service, ILocalIdTracker localIdTracker, IResourceContextProvider resourceContextProvider) + public CreateProcessor(ICreateService service, ILocalIdTracker localIdTracker, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(service, nameof(service)); ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _service = service; _localIdTracker = localIdTracker; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } /// @@ -37,7 +37,7 @@ public virtual async Task ProcessAsync(OperationContainer op if (operation.Resource.LocalId != null) { string serverId = newResource != null ? newResource.StringId : operation.Resource.StringId; - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(); _localIdTracker.Assign(operation.Resource.LocalId, resourceContext.PublicName, serverId); } diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index bee40d62c9..4b5d36c421 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -1,9 +1,8 @@ using System; using System.Data; +using System.Text.Json; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Configuration { @@ -12,15 +11,6 @@ namespace JsonApiDotNetCore.Configuration /// public interface IJsonApiOptions { - internal NamingStrategy SerializerNamingStrategy - { - get - { - var contractResolver = SerializerSettings.ContractResolver as DefaultContractResolver; - return contractResolver?.NamingStrategy ?? JsonApiOptions.DefaultNamingStrategy; - } - } - /// /// The URL prefix to use for exposed endpoints. /// @@ -40,7 +30,7 @@ internal NamingStrategy SerializerNamingStrategy bool IncludeJsonApiVersion { get; } /// - /// Whether or not stack traces should be serialized in objects. False by default. + /// Whether or not stack traces should be serialized in . False by default. /// bool IncludeExceptionStackTraceInErrors { get; } @@ -88,7 +78,7 @@ internal NamingStrategy SerializerNamingStrategy LinkTypes RelationshipLinks { get; } /// - /// Whether or not the total resource count should be included in all document-level meta objects. False by default. + /// Whether or not the total resource count should be included in top-level meta objects. This requires an additional database query. False by default. /// bool IncludeTotalResourceCount { get; } @@ -128,18 +118,6 @@ internal NamingStrategy SerializerNamingStrategy /// bool EnableLegacyFilterNotation { get; } - /// - /// Determines whether the serialization setting can be controlled using a query string - /// parameter. False by default. - /// - bool AllowQueryStringOverrideForSerializerNullValueHandling { get; } - - /// - /// Determines whether the serialization setting can be controlled using a query string - /// parameter. False by default. - /// - bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; } - /// /// Controls how many levels deep includes are allowed to be nested. For example, MaximumIncludeDepth=1 would allow ?include=articles but not /// ?include=articles.revisions. null by default, which means unconstrained. @@ -158,18 +136,25 @@ internal NamingStrategy SerializerNamingStrategy IsolationLevel? TransactionIsolationLevel { get; } /// - /// Specifies the settings that are used by the . Note that at some places a few settings are ignored, to ensure JSON:API - /// spec compliance. + /// Enables to customize the settings that are used by the . + /// /// - /// The next example changes the naming convention to kebab casing. + /// The next example sets the naming convention to camel casing. /// /// + JsonSerializerOptions SerializerOptions { get; } + + /// + /// Gets the settings used for deserializing request bodies. This value is based on and is intended for internal use. + /// + JsonSerializerOptions SerializerReadOptions { get; } + + /// + /// Gets the settings used for serializing response bodies. This value is based on and is intended for internal use. /// - JsonSerializerSettings SerializerSettings { get; } + JsonSerializerOptions SerializerWriteOptions { get; } } } diff --git a/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs b/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs deleted file mode 100644 index 62f4e729a6..0000000000 --- a/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Configuration -{ - /// - /// Responsible for getting s from the . - /// - public interface IResourceContextProvider - { - /// - /// Gets the metadata for all registered resources. - /// - IReadOnlySet GetResourceContexts(); - - /// - /// Gets the resource metadata for the resource that is publicly exposed by the specified name. - /// - ResourceContext GetResourceContext(string publicName); - - /// - /// Gets the resource metadata for the specified resource type. - /// - ResourceContext GetResourceContext(Type resourceType); - - /// - /// Gets the resource metadata for the specified resource type. - /// - ResourceContext GetResourceContext() - where TResource : class, IIdentifiable; - } -} diff --git a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs index 3629b1b69a..b7216f0f8c 100644 --- a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs @@ -8,14 +8,46 @@ namespace JsonApiDotNetCore.Configuration { /// - /// Enables retrieving the exposed resource fields (attributes and relationships) of resources registered in the resource graph. + /// Metadata about the shape of JSON:API resources that your API serves and the relationships between them. The resource graph is built at application + /// startup and is exposed as a singleton through Dependency Injection. /// [PublicAPI] - public interface IResourceGraph : IResourceContextProvider + public interface IResourceGraph { /// - /// Gets all fields (attributes and relationships) for that are targeted by the selector. If no selector is provided, - /// all exposed fields are returned. + /// Gets the metadata for all registered resources. + /// + IReadOnlySet GetResourceContexts(); + + /// + /// Gets the resource metadata for the resource that is publicly exposed by the specified name. Throws an when + /// not found. + /// + ResourceContext GetResourceContext(string publicName); + + /// + /// Gets the resource metadata for the specified resource type. Throws an when not found. + /// + ResourceContext GetResourceContext(Type resourceType); + + /// + /// Gets the resource metadata for the specified resource type. Throws an when not found. + /// + ResourceContext GetResourceContext() + where TResource : class, IIdentifiable; + + /// + /// Attempts to get the resource metadata for the resource that is publicly exposed by the specified name. Returns null when not found. + /// + ResourceContext TryGetResourceContext(string publicName); + + /// + /// Attempts to get the resource metadata for the specified resource type. Returns null when not found. + /// + ResourceContext TryGetResourceContext(Type resourceType); + + /// + /// Gets the fields (attributes and relationships) for that are targeted by the selector. /// /// /// The resource type for which to retrieve fields. @@ -27,8 +59,7 @@ IReadOnlyCollection GetFields(Expression - /// Gets all attributes for that are targeted by the selector. If no selector is provided, all exposed attributes are - /// returned. + /// Gets the attributes for that are targeted by the selector. /// /// /// The resource type for which to retrieve attributes. @@ -40,8 +71,7 @@ IReadOnlyCollection GetAttributes(Expression - /// Gets all relationships for that are targeted by the selector. If no selector is provided, all exposed relationships - /// are returned. + /// Gets the relationships for that are targeted by the selector. /// /// /// The resource type for which to retrieve relationships. diff --git a/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs b/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs index 27a70d314c..abe95d00bf 100644 --- a/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs +++ b/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs @@ -12,15 +12,15 @@ namespace JsonApiDotNetCore.Configuration [PublicAPI] public sealed class InverseNavigationResolver : IInverseNavigationResolver { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IEnumerable _dbContextResolvers; - public InverseNavigationResolver(IResourceContextProvider resourceContextProvider, IEnumerable dbContextResolvers) + public InverseNavigationResolver(IResourceGraph resourceGraph, IEnumerable dbContextResolvers) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(dbContextResolvers, nameof(dbContextResolvers)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _dbContextResolvers = dbContextResolvers; } @@ -36,7 +36,7 @@ public void Resolve() private void Resolve(DbContext dbContext) { - foreach (ResourceContext resourceContext in _resourceContextProvider.GetResourceContexts().Where(context => context.Relationships.Any())) + foreach (ResourceContext resourceContext in _resourceGraph.GetResourceContexts().Where(context => context.Relationships.Any())) { IEntityType entityType = dbContext.Model.FindEntityType(resourceContext.ResourceType); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 6419e0327c..9200149b25 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -12,6 +12,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Building; +using JsonApiDotNetCore.Serialization.JsonConverters; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -87,6 +88,9 @@ public void AddResourceGraph(ICollection dbContextTypes, Action(); _services.AddScoped(); - _services.AddSingleton(sp => sp.GetRequiredService()); } private void AddRepositoryLayer() @@ -218,8 +221,6 @@ private void AddQueryStringLayer() _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); _services.AddScoped(); RegisterDependentService(); @@ -227,8 +228,6 @@ private void AddQueryStringLayer() RegisterDependentService(); RegisterDependentService(); RegisterDependentService(); - RegisterDependentService(); - RegisterDependentService(); RegisterDependentService(); RegisterDependentService(); @@ -253,7 +252,6 @@ private void AddSerializationLayer() { _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index b898b9e205..4806c36248 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -1,8 +1,11 @@ +using System; using System.Data; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Threading; using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; +using JsonApiDotNetCore.Serialization.JsonConverters; namespace JsonApiDotNetCore.Configuration { @@ -10,7 +13,14 @@ namespace JsonApiDotNetCore.Configuration [PublicAPI] public sealed class JsonApiOptions : IJsonApiOptions { - internal static readonly NamingStrategy DefaultNamingStrategy = new CamelCaseNamingStrategy(); + private Lazy _lazySerializerWriteOptions; + private Lazy _lazySerializerReadOptions; + + /// + JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => _lazySerializerReadOptions.Value; + + /// + JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => _lazySerializerWriteOptions.Value; // Workaround for https://github.com/dotnet/efcore/issues/21026 internal bool DisableTopPagination { get; set; } @@ -64,12 +74,6 @@ public sealed class JsonApiOptions : IJsonApiOptions /// public bool EnableLegacyFilterNotation { get; set; } - /// - public bool AllowQueryStringOverrideForSerializerNullValueHandling { get; set; } - - /// - public bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; set; } - /// public int? MaximumIncludeDepth { get; set; } @@ -80,12 +84,37 @@ public sealed class JsonApiOptions : IJsonApiOptions public IsolationLevel? TransactionIsolationLevel { get; set; } /// - public JsonSerializerSettings SerializerSettings { get; } = new() + public JsonSerializerOptions SerializerOptions { get; } = new() { - ContractResolver = new DefaultContractResolver + // These are the options common to serialization and deserialization. + // At runtime, we actually use SerializerReadOptions and SerializerWriteOptions, which are customized copies of these settings, + // to overcome the limitation in System.Text.Json that the JsonPath is incorrect when using custom converters. + // Therefore we try to avoid using custom converters has much as possible. + // https://github.com/Tarmil/FSharp.SystemTextJson/issues/37 + // https://github.com/dotnet/runtime/issues/50205#issuecomment-808401245 + + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Converters = { - NamingStrategy = DefaultNamingStrategy + new SingleOrManyDataConverterFactory() } }; + + public JsonApiOptions() + { + _lazySerializerReadOptions = + new Lazy(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.PublicationOnly); + + _lazySerializerWriteOptions = new Lazy(() => new JsonSerializerOptions(SerializerOptions) + { + Converters = + { + new WriteOnlyDocumentConverter(), + new WriteOnlyRelationshipObjectConverter() + } + }, LazyThreadSafetyMode.PublicationOnly); + } } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs index f4d57f135c..1a661aaf1e 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs @@ -52,7 +52,7 @@ public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEnt private static bool IsId(string key) { - return key == nameof(Identifiable.Id) || key.EndsWith("." + nameof(Identifiable.Id), StringComparison.Ordinal); + return key == nameof(Identifiable.Id) || key.EndsWith($".{nameof(Identifiable.Id)}", StringComparison.Ordinal); } private static bool IsAtPrimaryEndpoint(IJsonApiRequest request) diff --git a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs index 5a18a8c2f1..f5b42e5499 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs @@ -7,12 +7,13 @@ namespace JsonApiDotNetCore.Configuration { /// - /// Provides metadata for a resource, such as its attributes and relationships. + /// Metadata about the shape of a JSON:API resource in the resource graph. /// [PublicAPI] public sealed class ResourceContext { - private IReadOnlyCollection _fields; + private readonly Dictionary _fieldsByPublicName = new(); + private readonly Dictionary _fieldsByPropertyName = new(); /// /// The publicly exposed resource name. @@ -29,6 +30,11 @@ public sealed class ResourceContext /// public Type IdentityType { get; } + /// + /// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields. + /// + public IReadOnlyCollection Fields { get; } + /// /// Exposed resource attributes. See https://jsonapi.org/format/#document-resource-object-attributes. /// @@ -44,11 +50,6 @@ public sealed class ResourceContext /// public IReadOnlyCollection EagerLoads { get; } - /// - /// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields. - /// - public IReadOnlyCollection Fields => _fields ??= Attributes.Cast().Concat(Relationships).ToArray(); - /// /// Configures which links to show in the object for this resource type. Defaults to /// , which falls back to . @@ -92,12 +93,79 @@ public ResourceContext(string publicName, Type resourceType, Type identityType, PublicName = publicName; ResourceType = resourceType; IdentityType = identityType; + Fields = attributes.Cast().Concat(relationships).ToArray(); Attributes = attributes; Relationships = relationships; EagerLoads = eagerLoads; TopLevelLinks = topLevelLinks; ResourceLinks = resourceLinks; RelationshipLinks = relationshipLinks; + + foreach (ResourceFieldAttribute field in Fields) + { + _fieldsByPublicName.Add(field.PublicName, field); + _fieldsByPropertyName.Add(field.Property.Name, field); + } + } + + public AttrAttribute GetAttributeByPublicName(string publicName) + { + AttrAttribute attribute = TryGetAttributeByPublicName(publicName); + return attribute ?? throw new InvalidOperationException($"Attribute '{publicName}' does not exist on resource type '{PublicName}'."); + } + + public AttrAttribute TryGetAttributeByPublicName(string publicName) + { + ArgumentGuard.NotNull(publicName, nameof(publicName)); + + return _fieldsByPublicName.TryGetValue(publicName, out ResourceFieldAttribute field) && field is AttrAttribute attribute ? attribute : null; + } + + public AttrAttribute GetAttributeByPropertyName(string propertyName) + { + AttrAttribute attribute = TryGetAttributeByPropertyName(propertyName); + + return attribute ?? + throw new InvalidOperationException($"Attribute for property '{propertyName}' does not exist on resource type '{ResourceType.Name}'."); + } + + public AttrAttribute TryGetAttributeByPropertyName(string propertyName) + { + ArgumentGuard.NotNull(propertyName, nameof(propertyName)); + + return _fieldsByPropertyName.TryGetValue(propertyName, out ResourceFieldAttribute field) && field is AttrAttribute attribute ? attribute : null; + } + + public RelationshipAttribute GetRelationshipByPublicName(string publicName) + { + RelationshipAttribute relationship = TryGetRelationshipByPublicName(publicName); + return relationship ?? throw new InvalidOperationException($"Relationship '{publicName}' does not exist on resource type '{PublicName}'."); + } + + public RelationshipAttribute TryGetRelationshipByPublicName(string publicName) + { + ArgumentGuard.NotNull(publicName, nameof(publicName)); + + return _fieldsByPublicName.TryGetValue(publicName, out ResourceFieldAttribute field) && field is RelationshipAttribute relationship + ? relationship + : null; + } + + public RelationshipAttribute GetRelationshipByPropertyName(string propertyName) + { + RelationshipAttribute relationship = TryGetRelationshipByPropertyName(propertyName); + + return relationship ?? + throw new InvalidOperationException($"Relationship for property '{propertyName}' does not exist on resource type '{ResourceType.Name}'."); + } + + public RelationshipAttribute TryGetRelationshipByPropertyName(string propertyName) + { + ArgumentGuard.NotNull(propertyName, nameof(propertyName)); + + return _fieldsByPropertyName.TryGetValue(propertyName, out ResourceFieldAttribute field) && field is RelationshipAttribute relationship + ? relationship + : null; } public override string ToString() diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs index 26ada5f798..f65755b38d 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs @@ -14,37 +14,71 @@ namespace JsonApiDotNetCore.Configuration public sealed class ResourceGraph : IResourceGraph { private static readonly Type ProxyTargetAccessorType = Type.GetType("Castle.DynamicProxy.IProxyTargetAccessor, Castle.Core"); - private readonly IReadOnlySet _resourceContexts; + + private readonly IReadOnlySet _resourceContextSet; + private readonly Dictionary _resourceContextsByType = new(); + private readonly Dictionary _resourceContextsByPublicName = new(); public ResourceGraph(IReadOnlySet resourceContexts) { ArgumentGuard.NotNull(resourceContexts, nameof(resourceContexts)); - _resourceContexts = resourceContexts; + _resourceContextSet = resourceContexts; + + foreach (ResourceContext resourceContext in resourceContexts) + { + _resourceContextsByType.Add(resourceContext.ResourceType, resourceContext); + _resourceContextsByPublicName.Add(resourceContext.PublicName, resourceContext); + } } /// public IReadOnlySet GetResourceContexts() { - return _resourceContexts; + return _resourceContextSet; } /// public ResourceContext GetResourceContext(string publicName) + { + ResourceContext resourceContext = TryGetResourceContext(publicName); + + if (resourceContext == null) + { + throw new InvalidOperationException($"Resource type '{publicName}' does not exist."); + } + + return resourceContext; + } + + /// + public ResourceContext TryGetResourceContext(string publicName) { ArgumentGuard.NotNullNorEmpty(publicName, nameof(publicName)); - return _resourceContexts.SingleOrDefault(resourceContext => resourceContext.PublicName == publicName); + return _resourceContextsByPublicName.TryGetValue(publicName, out ResourceContext resourceContext) ? resourceContext : null; } /// public ResourceContext GetResourceContext(Type resourceType) + { + ResourceContext resourceContext = TryGetResourceContext(resourceType); + + if (resourceContext == null) + { + throw new InvalidOperationException($"Resource of type '{resourceType.Name}' does not exist."); + } + + return resourceContext; + } + + /// + public ResourceContext TryGetResourceContext(Type resourceType) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - return IsLazyLoadingProxyForResourceType(resourceType) - ? _resourceContexts.SingleOrDefault(resourceContext => resourceContext.ResourceType == resourceType.BaseType) - : _resourceContexts.SingleOrDefault(resourceContext => resourceContext.ResourceType == resourceType); + Type typeToFind = IsLazyLoadingProxyForResourceType(resourceType) ? resourceType.BaseType : resourceType; + return _resourceContextsByType.TryGetValue(typeToFind!, out ResourceContext resourceContext) ? resourceContext : null; } private bool IsLazyLoadingProxyForResourceType(Type resourceType) diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 24bf42dc24..0ea1fb1a14 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -243,13 +243,15 @@ private Type TypeOrElementType(Type type) private string FormatResourceName(Type resourceType) { - var formatter = new ResourceNameFormatter(_options.SerializerNamingStrategy); + var formatter = new ResourceNameFormatter(_options.SerializerOptions.PropertyNamingPolicy); return formatter.FormatResourceName(resourceType); } private string FormatPropertyName(PropertyInfo resourceProperty) { - return _options.SerializerNamingStrategy.GetPropertyName(resourceProperty.Name, false); + return _options.SerializerOptions.PropertyNamingPolicy == null + ? resourceProperty.Name + : _options.SerializerOptions.PropertyNamingPolicy.ConvertName(resourceProperty.Name); } } } diff --git a/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs b/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs index 70963bb1a3..93e5dda4ff 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs @@ -1,18 +1,18 @@ using System; using System.Reflection; +using System.Text.Json; using Humanizer; using JsonApiDotNetCore.Resources.Annotations; -using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Configuration { internal sealed class ResourceNameFormatter { - private readonly NamingStrategy _namingStrategy; + private readonly JsonNamingPolicy _namingPolicy; - public ResourceNameFormatter(NamingStrategy namingStrategy) + public ResourceNameFormatter(JsonNamingPolicy namingPolicy) { - _namingStrategy = namingStrategy; + _namingPolicy = namingPolicy; } /// @@ -20,9 +20,13 @@ public ResourceNameFormatter(NamingStrategy namingStrategy) /// public string FormatResourceName(Type resourceType) { - return resourceType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute - ? attribute.PublicName - : _namingStrategy.GetPropertyName(resourceType.Name.Pluralize(), false); + if (resourceType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) + { + return attribute.PublicName; + } + + string publicName = resourceType.Name.Pluralize(); + return _namingPolicy != null ? _namingPolicy.ConvertName(publicName) : publicName; } } } diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index b05fde176e..2c8c1fc5a2 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -6,8 +6,6 @@ using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Client.Internal; using JsonApiDotNetCore.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -57,25 +55,6 @@ private static void SetupApplicationBuilder(IServiceCollection services, Action< applicationBuilder.ConfigureServiceContainer(dbContextTypes); } - /// - /// Enables client serializers for sending requests and receiving responses in JSON:API format. Internally only used for testing. Will be extended in the - /// future to be part of a JsonApiClientDotNetCore package. - /// - public static IServiceCollection AddClientSerialization(this IServiceCollection services) - { - ArgumentGuard.NotNull(services, nameof(services)); - - services.AddScoped(); - - services.AddScoped(sp => - { - var graph = sp.GetRequiredService(); - return new RequestSerializer(graph, new ResourceObjectBuilder(graph, new ResourceObjectBuilderSettings())); - }); - - return services; - } - /// /// Adds IoC container registrations for the various JsonApiDotNetCore resource service interfaces, such as , /// and the various others. diff --git a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs index 5aa436bc22..7f6d266aa4 100644 --- a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs @@ -67,14 +67,14 @@ public ResourceDescriptor TryGetResourceDescriptor(Type type) if (!openGenericInterface.IsInterface || !openGenericInterface.IsGenericType || openGenericInterface != openGenericInterface.GetGenericTypeDefinition()) { - throw new ArgumentException($"Specified type '{openGenericInterface.FullName}' " + "is not an open generic interface.", + throw new ArgumentException($"Specified type '{openGenericInterface.FullName}' is not an open generic interface.", nameof(openGenericInterface)); } if (interfaceGenericTypeArguments.Length != openGenericInterface.GetGenericArguments().Length) { throw new ArgumentException( - $"Interface '{openGenericInterface.FullName}' " + $"requires {openGenericInterface.GetGenericArguments().Length} type parameters " + + $"Interface '{openGenericInterface.FullName}' requires {openGenericInterface.GetGenericArguments().Length} type parameters " + $"instead of {interfaceGenericTypeArguments.Length}.", nameof(interfaceGenericTypeArguments)); } diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index c7bddff7b6..98a4c58afe 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -190,7 +190,7 @@ public virtual async Task PostAsync([FromBody] TResource resource if (_options.ValidateModelState && !ModelState.IsValid) { throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, - _options.SerializerNamingStrategy); + _options.SerializerOptions.PropertyNamingPolicy); } TResource newResource = await _create.CreateAsync(resource, cancellationToken); @@ -267,7 +267,7 @@ public virtual async Task PatchAsync(TId id, [FromBody] TResource if (_options.ValidateModelState && !ModelState.IsValid) { throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, - _options.SerializerNamingStrategy); + _options.SerializerOptions.PropertyNamingPolicy); } TResource updated = await _update.UpdateAsync(id, resource, cancellationToken); diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs index 64feb432ee..0ed536eb15 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs @@ -174,7 +174,7 @@ protected virtual void ValidateModelState(IEnumerable operat if (violations.Any()) { - throw new InvalidModelStateException(violations, _options.IncludeExceptionStackTraceInErrors, _options.SerializerNamingStrategy); + throw new InvalidModelStateException(violations, _options.IncludeExceptionStackTraceInErrors, _options.SerializerOptions.PropertyNamingPolicy); } } diff --git a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs index 17db572550..88d9614cc7 100644 --- a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc; @@ -9,18 +10,21 @@ namespace JsonApiDotNetCore.Controllers /// public abstract class CoreJsonApiController : ControllerBase { - protected IActionResult Error(Error error) + protected IActionResult Error(ErrorObject error) { ArgumentGuard.NotNull(error, nameof(error)); return Error(error.AsEnumerable()); } - protected IActionResult Error(IEnumerable errors) + protected IActionResult Error(IEnumerable errors) { ArgumentGuard.NotNull(errors, nameof(errors)); - var document = new ErrorDocument(errors); + var document = new Document + { + Errors = errors.ToList() + }; return new ObjectResult(document) { diff --git a/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs b/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs index f528ed6c96..782cf1f2ea 100644 --- a/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs +++ b/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Errors public sealed class CannotClearRequiredRelationshipException : JsonApiException { public CannotClearRequiredRelationshipException(string relationshipName, string resourceId, string resourceType) - : base(new Error(HttpStatusCode.BadRequest) + : base(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Failed to clear a required relationship.", Detail = $"The relationship '{relationshipName}' of resource type '{resourceType}' " + diff --git a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs index 7f4f3ee004..4ca8586b17 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs @@ -4,12 +4,12 @@ using System.Linq; using System.Net; using System.Reflection; +using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Errors { @@ -20,13 +20,13 @@ namespace JsonApiDotNetCore.Errors public sealed class InvalidModelStateException : JsonApiException { public InvalidModelStateException(ModelStateDictionary modelState, Type resourceType, bool includeExceptionStackTraceInErrors, - NamingStrategy namingStrategy) - : this(FromModelStateDictionary(modelState, resourceType), includeExceptionStackTraceInErrors, namingStrategy) + JsonNamingPolicy namingPolicy) + : this(FromModelStateDictionary(modelState, resourceType), includeExceptionStackTraceInErrors, namingPolicy) { } - public InvalidModelStateException(IEnumerable violations, bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy) - : base(FromModelStateViolations(violations, includeExceptionStackTraceInErrors, namingStrategy)) + public InvalidModelStateException(IEnumerable violations, bool includeExceptionStackTraceInErrors, JsonNamingPolicy namingPolicy) + : base(FromModelStateViolations(violations, includeExceptionStackTraceInErrors, namingPolicy)) { } @@ -54,50 +54,55 @@ private static void AddValidationErrors(ModelStateEntry entry, string propertyNa } } - private static IEnumerable FromModelStateViolations(IEnumerable violations, bool includeExceptionStackTraceInErrors, - NamingStrategy namingStrategy) + private static IEnumerable FromModelStateViolations(IEnumerable violations, bool includeExceptionStackTraceInErrors, + JsonNamingPolicy namingPolicy) { ArgumentGuard.NotNull(violations, nameof(violations)); - ArgumentGuard.NotNull(namingStrategy, nameof(namingStrategy)); - return violations.SelectMany(violation => FromModelStateViolation(violation, includeExceptionStackTraceInErrors, namingStrategy)); + return violations.SelectMany(violation => FromModelStateViolation(violation, includeExceptionStackTraceInErrors, namingPolicy)); } - private static IEnumerable FromModelStateViolation(ModelStateViolation violation, bool includeExceptionStackTraceInErrors, - NamingStrategy namingStrategy) + private static IEnumerable FromModelStateViolation(ModelStateViolation violation, bool includeExceptionStackTraceInErrors, + JsonNamingPolicy namingPolicy) { if (violation.Error.Exception is JsonApiException jsonApiException) { - foreach (Error error in jsonApiException.Errors) + foreach (ErrorObject error in jsonApiException.Errors) { yield return error; } } else { - string attributeName = GetDisplayNameForProperty(violation.PropertyName, violation.ResourceType, namingStrategy); - string attributePath = violation.Prefix + attributeName; + string attributeName = GetDisplayNameForProperty(violation.PropertyName, violation.ResourceType, namingPolicy); + string attributePath = $"{violation.Prefix}{attributeName}"; yield return FromModelError(violation.Error, attributePath, includeExceptionStackTraceInErrors); } } - private static string GetDisplayNameForProperty(string propertyName, Type resourceType, NamingStrategy namingStrategy) + private static string GetDisplayNameForProperty(string propertyName, Type resourceType, JsonNamingPolicy namingPolicy) { PropertyInfo property = resourceType.GetProperty(propertyName); if (property != null) { var attrAttribute = property.GetCustomAttribute(); - return attrAttribute?.PublicName ?? namingStrategy.GetPropertyName(property.Name, false); + + if (attrAttribute?.PublicName != null) + { + return attrAttribute.PublicName; + } + + return namingPolicy != null ? namingPolicy.ConvertName(property.Name) : property.Name; } return propertyName; } - private static Error FromModelError(ModelError modelError, string attributePath, bool includeExceptionStackTraceInErrors) + private static ErrorObject FromModelError(ModelError modelError, string attributePath, bool includeExceptionStackTraceInErrors) { - var error = new Error(HttpStatusCode.UnprocessableEntity) + var error = new ErrorObject(HttpStatusCode.UnprocessableEntity) { Title = "Input validation failed.", Detail = modelError.ErrorMessage, @@ -111,7 +116,10 @@ private static Error FromModelError(ModelError modelError, string attributePath, if (includeExceptionStackTraceInErrors && modelError.Exception != null) { - error.Meta.IncludeExceptionStackTrace(modelError.Exception.Demystify()); + string[] stackTraceLines = modelError.Exception.Demystify().ToString().Split(Environment.NewLine); + + error.Meta ??= new Dictionary(); + error.Meta["StackTrace"] = stackTraceLines; } return error; diff --git a/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs b/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs index 5e704b8e3c..fb02f2a5e4 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs @@ -13,7 +13,7 @@ namespace JsonApiDotNetCore.Errors public sealed class InvalidQueryException : JsonApiException { public InvalidQueryException(string reason, Exception exception) - : base(new Error(HttpStatusCode.BadRequest) + : base(new ErrorObject(HttpStatusCode.BadRequest) { Title = reason, Detail = exception?.Message diff --git a/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs b/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs index 99a4eb381e..949710d62a 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs @@ -14,11 +14,11 @@ public sealed class InvalidQueryStringParameterException : JsonApiException public string QueryParameterName { get; } public InvalidQueryStringParameterException(string queryParameterName, string genericMessage, string specificMessage, Exception innerException = null) - : base(new Error(HttpStatusCode.BadRequest) + : base(new ErrorObject(HttpStatusCode.BadRequest) { Title = genericMessage, Detail = specificMessage, - Source = + Source = new ErrorSource { Parameter = queryParameterName } diff --git a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs index fe860ac0fd..c435c66b2d 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs @@ -13,9 +13,9 @@ namespace JsonApiDotNetCore.Errors public sealed class InvalidRequestBodyException : JsonApiException { public InvalidRequestBodyException(string reason, string details, string requestBody, Exception innerException = null) - : base(new Error(HttpStatusCode.UnprocessableEntity) + : base(new ErrorObject(HttpStatusCode.UnprocessableEntity) { - Title = reason != null ? "Failed to deserialize request body: " + reason : "Failed to deserialize request body.", + Title = reason != null ? $"Failed to deserialize request body: {reason}" : "Failed to deserialize request body.", Detail = FormatErrorDetail(details, requestBody, innerException) }, innerException) { diff --git a/src/JsonApiDotNetCore/Errors/JsonApiException.cs b/src/JsonApiDotNetCore/Errors/JsonApiException.cs index 6ad1a2021c..13d3b6a745 100644 --- a/src/JsonApiDotNetCore/Errors/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Errors/JsonApiException.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Errors { @@ -13,17 +15,18 @@ namespace JsonApiDotNetCore.Errors [PublicAPI] public class JsonApiException : Exception { - private static readonly JsonSerializerSettings ErrorSerializerSettings = new() + private static readonly JsonSerializerOptions SerializerOptions = new() { - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.Indented + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; - public IReadOnlyList Errors { get; } + public IReadOnlyList Errors { get; } - public override string Message => "Errors = " + JsonConvert.SerializeObject(Errors, ErrorSerializerSettings); + public override string Message => $"Errors = {JsonSerializer.Serialize(Errors, SerializerOptions)}"; - public JsonApiException(Error error, Exception innerException = null) + public JsonApiException(ErrorObject error, Exception innerException = null) : base(null, innerException) { ArgumentGuard.NotNull(error, nameof(error)); @@ -31,10 +34,10 @@ public JsonApiException(Error error, Exception innerException = null) Errors = error.AsArray(); } - public JsonApiException(IEnumerable errors, Exception innerException = null) + public JsonApiException(IEnumerable errors, Exception innerException = null) : base(null, innerException) { - List errorList = errors?.ToList(); + List errorList = errors?.ToList(); ArgumentGuard.NotNullNorEmpty(errorList, nameof(errors)); Errors = errorList; diff --git a/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs b/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs index e092150ba5..81e13baeda 100644 --- a/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs +++ b/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs @@ -11,10 +11,10 @@ namespace JsonApiDotNetCore.Errors public sealed class MissingTransactionSupportException : JsonApiException { public MissingTransactionSupportException(string resourceType) - : base(new Error(HttpStatusCode.UnprocessableEntity) + : base(new ErrorObject(HttpStatusCode.UnprocessableEntity) { Title = "Unsupported resource type in atomic:operations request.", - Detail = $"Operations on resources of type '{resourceType}' " + "cannot be used because transaction support is unavailable." + Detail = $"Operations on resources of type '{resourceType}' cannot be used because transaction support is unavailable." }) { } diff --git a/src/JsonApiDotNetCore/Errors/NonParticipatingTransactionException.cs b/src/JsonApiDotNetCore/Errors/NonParticipatingTransactionException.cs index 9da29c521b..909f4a047d 100644 --- a/src/JsonApiDotNetCore/Errors/NonParticipatingTransactionException.cs +++ b/src/JsonApiDotNetCore/Errors/NonParticipatingTransactionException.cs @@ -11,10 +11,10 @@ namespace JsonApiDotNetCore.Errors public sealed class NonParticipatingTransactionException : JsonApiException { public NonParticipatingTransactionException() - : base(new Error(HttpStatusCode.UnprocessableEntity) + : base(new ErrorObject(HttpStatusCode.UnprocessableEntity) { Title = "Unsupported combination of resource types in atomic:operations request.", - Detail = "All operations need to participate in a single shared transaction, " + "which is not the case for this request." + Detail = "All operations need to participate in a single shared transaction, which is not the case for this request." }) { } diff --git a/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs b/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs index 864a0c7606..07b5d0e25a 100644 --- a/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Errors public sealed class RelationshipNotFoundException : JsonApiException { public RelationshipNotFoundException(string relationshipName, string resourceType) - : base(new Error(HttpStatusCode.NotFound) + : base(new ErrorObject(HttpStatusCode.NotFound) { Title = "The requested relationship does not exist.", Detail = $"Resource of type '{resourceType}' does not contain a relationship named '{relationshipName}'." diff --git a/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs b/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs index 3d3c5b163b..a4edbc0d5f 100644 --- a/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs +++ b/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs @@ -14,7 +14,7 @@ public sealed class RequestMethodNotAllowedException : JsonApiException public HttpMethod Method { get; } public RequestMethodNotAllowedException(HttpMethod method) - : base(new Error(HttpStatusCode.MethodNotAllowed) + : base(new ErrorObject(HttpStatusCode.MethodNotAllowed) { Title = "The request method is not allowed.", Detail = $"Endpoint does not support {method} requests." diff --git a/src/JsonApiDotNetCore/Errors/ResourceAlreadyExistsException.cs b/src/JsonApiDotNetCore/Errors/ResourceAlreadyExistsException.cs index 34bc21dff5..384289576d 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceAlreadyExistsException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceAlreadyExistsException.cs @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Errors public sealed class ResourceAlreadyExistsException : JsonApiException { public ResourceAlreadyExistsException(string resourceId, string resourceType) - : base(new Error(HttpStatusCode.Conflict) + : base(new ErrorObject(HttpStatusCode.Conflict) { Title = "Another resource with the specified ID already exists.", Detail = $"Another resource of type '{resourceType}' with ID '{resourceId}' already exists." diff --git a/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs index 4266f987b3..11a96cc436 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs @@ -11,12 +11,12 @@ namespace JsonApiDotNetCore.Errors public sealed class ResourceIdInCreateResourceNotAllowedException : JsonApiException { public ResourceIdInCreateResourceNotAllowedException(int? atomicOperationIndex = null) - : base(new Error(HttpStatusCode.Forbidden) + : base(new ErrorObject(HttpStatusCode.Forbidden) { Title = atomicOperationIndex == null ? "Specifying the resource ID in POST requests is not allowed." : "Specifying the resource ID in operations that create a resource is not allowed.", - Source = + Source = new ErrorSource { Pointer = atomicOperationIndex != null ? $"/atomic:operations[{atomicOperationIndex}]/data/id" : "/data/id" } diff --git a/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs index 721c17e7cd..fdfd6b6fe9 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs @@ -11,10 +11,10 @@ namespace JsonApiDotNetCore.Errors public sealed class ResourceIdMismatchException : JsonApiException { public ResourceIdMismatchException(string bodyId, string endpointId, string requestPath) - : base(new Error(HttpStatusCode.Conflict) + : base(new ErrorObject(HttpStatusCode.Conflict) { Title = "Resource ID mismatch between request body and endpoint URL.", - Detail = $"Expected resource ID '{endpointId}' in PATCH request body " + $"at endpoint '{requestPath}', instead of '{bodyId}'." + Detail = $"Expected resource ID '{endpointId}' in PATCH request body at endpoint '{requestPath}', instead of '{bodyId}'." }) { } diff --git a/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs b/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs index 4e63220269..50a05b70ff 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Errors public sealed class ResourceNotFoundException : JsonApiException { public ResourceNotFoundException(string resourceId, string resourceType) - : base(new Error(HttpStatusCode.NotFound) + : base(new ErrorObject(HttpStatusCode.NotFound) { Title = "The requested resource does not exist.", Detail = $"Resource of type '{resourceType}' with ID '{resourceId}' does not exist." diff --git a/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs index 83f28a14f9..9957694d0d 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs @@ -13,11 +13,11 @@ namespace JsonApiDotNetCore.Errors public sealed class ResourceTypeMismatchException : JsonApiException { public ResourceTypeMismatchException(HttpMethod method, string requestPath, ResourceContext expected, ResourceContext actual) - : base(new Error(HttpStatusCode.Conflict) + : base(new ErrorObject(HttpStatusCode.Conflict) { Title = "Resource type mismatch between request body and endpoint URL.", - Detail = $"Expected resource of type '{expected.PublicName}' in {method} " + - $"request body at endpoint '{requestPath}', instead of '{actual?.PublicName}'." + Detail = $"Expected resource of type '{expected.PublicName}' in {method} request body at endpoint " + + $"'{requestPath}', instead of '{actual.PublicName}'." }) { } diff --git a/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs b/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs index d749bca5fa..0cb1f9cecb 100644 --- a/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs @@ -17,7 +17,7 @@ public ResourcesInRelationshipsNotFoundException(IEnumerable - diff --git a/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs index 72d635b5f3..d7d6349b68 100644 --- a/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs @@ -26,11 +26,11 @@ public Task OnExceptionAsync(ExceptionContext context) if (context.HttpContext.IsJsonApiRequest()) { - ErrorDocument errorDocument = _exceptionHandler.HandleException(context.Exception); + Document document = _exceptionHandler.HandleException(context.Exception); - context.Result = new ObjectResult(errorDocument) + context.Result = new ObjectResult(document) { - StatusCode = (int)errorDocument.GetErrorStatusCode() + StatusCode = (int)document.GetErrorStatusCode() }; } diff --git a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs index 2006ad62de..5dc6bf6e5d 100644 --- a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs +++ b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Net; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -26,7 +27,7 @@ public ExceptionHandler(ILoggerFactory loggerFactory, IJsonApiOptions options) _logger = loggerFactory.CreateLogger(); } - public ErrorDocument HandleException(Exception exception) + public Document HandleException(Exception exception) { ArgumentGuard.NotNull(exception, nameof(exception)); @@ -69,33 +70,42 @@ protected virtual string GetLogMessage(Exception exception) return exception.Message; } - protected virtual ErrorDocument CreateErrorDocument(Exception exception) + protected virtual Document CreateErrorDocument(Exception exception) { ArgumentGuard.NotNull(exception, nameof(exception)); - IReadOnlyList errors = exception is JsonApiException jsonApiException ? jsonApiException.Errors : - exception is OperationCanceledException ? new Error((HttpStatusCode)499) + IReadOnlyList errors = exception is JsonApiException jsonApiException ? jsonApiException.Errors : + exception is OperationCanceledException ? new ErrorObject((HttpStatusCode)499) { Title = "Request execution was canceled." - }.AsArray() : new Error(HttpStatusCode.InternalServerError) + }.AsArray() : new ErrorObject(HttpStatusCode.InternalServerError) { Title = "An unhandled error occurred while processing this request.", Detail = exception.Message }.AsArray(); - foreach (Error error in errors) + foreach (ErrorObject error in errors) { ApplyOptions(error, exception); } - return new ErrorDocument(errors); + return new Document + { + Errors = errors.ToList() + }; } - private void ApplyOptions(Error error, Exception exception) + private void ApplyOptions(ErrorObject error, Exception exception) { Exception resultException = exception is InvalidModelStateException ? null : exception; - error.Meta.IncludeExceptionStackTrace(_options.IncludeExceptionStackTraceInErrors ? resultException : null); + if (resultException != null && _options.IncludeExceptionStackTraceInErrors) + { + string[] stackTraceLines = resultException.ToString().Split(Environment.NewLine); + + error.Meta ??= new Dictionary(); + error.Meta["StackTrace"] = stackTraceLines; + } } } } diff --git a/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs index 2521794c08..9f44e33a96 100644 --- a/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs +++ b/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs @@ -4,10 +4,10 @@ namespace JsonApiDotNetCore.Middleware { /// - /// Central place to handle all exceptions. Log them and translate into Error response. + /// Central place to handle all exceptions, such as log them and translate into error response. /// public interface IExceptionHandler { - ErrorDocument HandleException(Exception exception); + Document HandleException(Exception exception); } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index efc3613979..30ea5d9108 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -1,14 +1,13 @@ using System; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -17,7 +16,6 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Middleware { @@ -41,41 +39,41 @@ public JsonApiMiddleware(RequestDelegate next, IHttpContextAccessor httpContextA } public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options, - IJsonApiRequest request, IResourceContextProvider resourceContextProvider, ILogger logger) + IJsonApiRequest request, IResourceGraph resourceGraph, ILogger logger) { ArgumentGuard.NotNull(httpContext, nameof(httpContext)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(logger, nameof(logger)); using (CodeTimingSessionManager.Current.Measure("JSON:API middleware")) { - if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerSettings)) + if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerWriteOptions)) { return; } RouteValueDictionary routeValues = httpContext.GetRouteData().Values; - ResourceContext primaryResourceContext = CreatePrimaryResourceContext(httpContext, controllerResourceMapping, resourceContextProvider); + ResourceContext primaryResourceContext = TryCreatePrimaryResourceContext(httpContext, controllerResourceMapping, resourceGraph); if (primaryResourceContext != null) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerSettings) || - !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerSettings)) + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerWriteOptions) || + !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerWriteOptions)) { return; } - SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, resourceContextProvider, httpContext.Request); + SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, resourceGraph, httpContext.Request); httpContext.RegisterJsonApiRequest(); } else if (IsRouteForOperations(routeValues)) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerSettings) || - !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerSettings)) + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions) || + !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions)) { return; } @@ -102,13 +100,17 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin } } - private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonSerializerSettings serializerSettings) + private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonSerializerOptions serializerOptions) { if (httpContext.Request.Headers.ContainsKey(HeaderNames.IfMatch)) { - await FlushResponseAsync(httpContext.Response, serializerSettings, new Error(HttpStatusCode.PreconditionFailed) + await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.PreconditionFailed) { - Title = "Detection of mid-air edit collisions using ETags is not supported." + Title = "Detection of mid-air edit collisions using ETags is not supported.", + Source = new ErrorSource + { + Header = "If-Match" + } }); return false; @@ -117,8 +119,8 @@ private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, Jso return true; } - private static ResourceContext CreatePrimaryResourceContext(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, - IResourceContextProvider resourceContextProvider) + private static ResourceContext TryCreatePrimaryResourceContext(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, + IResourceGraph resourceGraph) { Endpoint endpoint = httpContext.GetEndpoint(); var controllerActionDescriptor = endpoint?.Metadata.GetMetadata(); @@ -130,7 +132,7 @@ private static ResourceContext CreatePrimaryResourceContext(HttpContext httpCont if (resourceType != null) { - return resourceContextProvider.GetResourceContext(resourceType); + return resourceGraph.GetResourceContext(resourceType); } } @@ -138,7 +140,7 @@ private static ResourceContext CreatePrimaryResourceContext(HttpContext httpCont } private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, - JsonSerializerSettings serializerSettings) + JsonSerializerOptions serializerOptions) { string contentType = httpContext.Request.ContentType; @@ -146,10 +148,14 @@ private static async Task ValidateContentTypeHeaderAsync(string allowedCon // Justification: Workaround for https://github.com/dotnet/aspnetcore/issues/32097 (fixed in .NET 6) if (contentType != null && contentType != allowedContentType) { - await FlushResponseAsync(httpContext.Response, serializerSettings, new Error(HttpStatusCode.UnsupportedMediaType) + await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.UnsupportedMediaType) { Title = "The specified Content-Type header value is not supported.", - Detail = $"Please specify '{allowedContentType}' instead of '{contentType}' " + "for the Content-Type header value." + Detail = $"Please specify '{allowedContentType}' instead of '{contentType}' for the Content-Type header value.", + Source = new ErrorSource + { + Header = "Content-Type" + } }); return false; @@ -159,7 +165,7 @@ private static async Task ValidateContentTypeHeaderAsync(string allowedCon } private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue allowedMediaTypeValue, HttpContext httpContext, - JsonSerializerSettings serializerSettings) + JsonSerializerOptions serializerOptions) { string[] acceptHeaders = httpContext.Request.Headers.GetCommaSeparatedValues("Accept"); @@ -192,10 +198,14 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a if (!seenCompatibleMediaType) { - await FlushResponseAsync(httpContext.Response, serializerSettings, new Error(HttpStatusCode.NotAcceptable) + await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.NotAcceptable) { Title = "The specified Accept header value does not contain any supported media types.", - Detail = $"Please include '{allowedMediaTypeValue}' in the Accept header values." + Detail = $"Please include '{allowedMediaTypeValue}' in the Accept header values.", + Source = new ErrorSource + { + Header = "Accept" + } }); return false; @@ -204,32 +214,22 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a return true; } - private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSerializerSettings serializerSettings, Error error) + private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSerializerOptions serializerOptions, ErrorObject error) { httpResponse.ContentType = HeaderConstants.MediaType; httpResponse.StatusCode = (int)error.StatusCode; - var serializer = JsonSerializer.CreateDefault(serializerSettings); - serializer.ApplyErrorSettings(); - - // https://github.com/JamesNK/Newtonsoft.Json/issues/1193 - await using (var stream = new MemoryStream()) + var errorDocument = new Document { - await using (var streamWriter = new StreamWriter(stream, leaveOpen: true)) - { - using var jsonWriter = new JsonTextWriter(streamWriter); - serializer.Serialize(jsonWriter, new ErrorDocument(error)); - } - - stream.Seek(0, SeekOrigin.Begin); - await stream.CopyToAsync(httpResponse.Body); - } + Errors = error.AsList() + }; + await JsonSerializer.SerializeAsync(httpResponse.Body, errorDocument, serializerOptions); await httpResponse.Body.FlushAsync(); } private static void SetupResourceRequest(JsonApiRequest request, ResourceContext primaryResourceContext, RouteValueDictionary routeValues, - IResourceContextProvider resourceContextProvider, HttpRequest httpRequest) + IResourceGraph resourceGraph, HttpRequest httpRequest) { request.IsReadOnly = httpRequest.Method == HttpMethod.Get.Method || httpRequest.Method == HttpMethod.Head.Method; request.PrimaryResource = primaryResourceContext; @@ -252,13 +252,12 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceContext // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - RelationshipAttribute requestRelationship = - primaryResourceContext.Relationships.SingleOrDefault(relationship => relationship.PublicName == relationshipName); + RelationshipAttribute requestRelationship = primaryResourceContext.TryGetRelationshipByPublicName(relationshipName); if (requestRelationship != null) { request.Relationship = requestRelationship; - request.SecondaryResource = resourceContextProvider.GetResourceContext(requestRelationship.RightType); + request.SecondaryResource = resourceGraph.GetResourceContext(requestRelationship.RightType); } } else diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index 51a831a5b6..af38f89dad 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -32,18 +32,18 @@ namespace JsonApiDotNetCore.Middleware public sealed class JsonApiRoutingConvention : IJsonApiRoutingConvention { private readonly IJsonApiOptions _options; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly Dictionary _registeredControllerNameByTemplate = new(); private readonly Dictionary _resourceContextPerControllerTypeMap = new(); private readonly Dictionary _controllerPerResourceContextMap = new(); - public JsonApiRoutingConvention(IJsonApiOptions options, IResourceContextProvider resourceContextProvider) + public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(options, nameof(options)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _options = options; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } /// @@ -64,7 +64,7 @@ public string GetControllerNameForResourceType(Type resourceType) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); if (_controllerPerResourceContextMap.TryGetValue(resourceContext, out ControllerModel controllerModel)) @@ -90,7 +90,7 @@ public void Apply(ApplicationModel application) if (resourceType != null) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(resourceType); if (resourceContext != null) { @@ -146,7 +146,10 @@ private string TemplateFromResource(ControllerModel model) /// private string TemplateFromController(ControllerModel model) { - string controllerName = _options.SerializerNamingStrategy.GetPropertyName(model.ControllerName, false); + string controllerName = _options.SerializerOptions.PropertyNamingPolicy == null + ? model.ControllerName + : _options.SerializerOptions.PropertyNamingPolicy.ConvertName(model.ControllerName); + return $"{_options.Namespace}/{controllerName}"; } diff --git a/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs b/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs index bd9d4f12ac..2ed2dbab48 100644 --- a/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs +++ b/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs @@ -2,12 +2,24 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Middleware { - internal sealed class TraceLogWriter + internal abstract class TraceLogWriter + { + protected static readonly JsonSerializerOptions SerializerOptions = new() + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + ReferenceHandler = ReferenceHandler.Preserve + }; + } + + internal sealed class TraceLogWriter : TraceLogWriter { private readonly ILogger _logger; @@ -126,12 +138,9 @@ private static string SerializeObject(object value) { try { - // It turns out setting ReferenceLoopHandling to something other than Error only takes longer to fail. - // This is because Newtonsoft.Json always tries to serialize the first element in a graph. And with - // EF Core models, that one is often recursive, resulting in either StackOverflowException or OutOfMemoryException. - return JsonConvert.SerializeObject(value, Formatting.Indented); + return JsonSerializer.Serialize(value, SerializerOptions); } - catch (JsonSerializationException) + catch (JsonException) { // Never crash as a result of logging, this is best-effort only. return "object"; diff --git a/src/JsonApiDotNetCore/Properties/launchSettings.json b/src/JsonApiDotNetCore/Properties/launchSettings.json deleted file mode 100644 index 233fb4a18e..0000000000 --- a/src/JsonApiDotNetCore/Properties/launchSettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:63521/", - "sslPort": 0 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "JsonApiDotNetCore": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:63522/" - } - } -} diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs index 5461fb994c..d375f72a16 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs @@ -13,19 +13,19 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing [PublicAPI] public class FilterParser : QueryExpressionParser { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IResourceFactory _resourceFactory; private readonly Action _validateSingleFieldCallback; private ResourceContext _resourceContextInScope; - public FilterParser(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, + public FilterParser(IResourceGraph resourceGraph, IResourceFactory resourceFactory, Action validateSingleFieldCallback = null) - : base(resourceContextProvider) + : base(resourceGraph) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _resourceFactory = resourceFactory; _validateSingleFieldCallback = validateSingleFieldCallback; } @@ -268,7 +268,7 @@ private FilterExpression ParseFilterInHas(HasManyAttribute hasManyRelationship) ResourceContext outerScopeBackup = _resourceContextInScope; Type innerResourceType = hasManyRelationship.RightType; - _resourceContextInScope = _resourceContextProvider.GetResourceContext(innerResourceType); + _resourceContextInScope = _resourceGraph.GetResourceContext(innerResourceType); FilterExpression filter = ParseFilter(); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs index b6cb69d6b9..9d6f394d75 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs @@ -17,9 +17,8 @@ public class IncludeParser : QueryExpressionParser private readonly Action _validateSingleRelationshipCallback; private ResourceContext _resourceContextInScope; - public IncludeParser(IResourceContextProvider resourceContextProvider, - Action validateSingleRelationshipCallback = null) - : base(resourceContextProvider) + public IncludeParser(IResourceGraph resourceGraph, Action validateSingleRelationshipCallback = null) + : base(resourceGraph) { _validateSingleRelationshipCallback = validateSingleRelationshipCallback; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs index 54c56b2f13..62f8dd6a91 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs @@ -14,9 +14,8 @@ public class PaginationParser : QueryExpressionParser private readonly Action _validateSingleFieldCallback; private ResourceContext _resourceContextInScope; - public PaginationParser(IResourceContextProvider resourceContextProvider, - Action validateSingleFieldCallback = null) - : base(resourceContextProvider) + public PaginationParser(IResourceGraph resourceGraph, Action validateSingleFieldCallback = null) + : base(resourceGraph) { _validateSingleFieldCallback = validateSingleFieldCallback; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs index 99a195bb6c..65cf321347 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs @@ -21,9 +21,9 @@ public abstract class QueryExpressionParser protected Stack TokenStack { get; private set; } private protected ResourceFieldChainResolver ChainResolver { get; } - protected QueryExpressionParser(IResourceContextProvider resourceContextProvider) + protected QueryExpressionParser(IResourceGraph resourceGraph) { - ChainResolver = new ResourceFieldChainResolver(resourceContextProvider); + ChainResolver = new ResourceFieldChainResolver(resourceGraph); } /// @@ -74,7 +74,7 @@ protected void EatText(string text) { if (!TokenStack.TryPop(out Token token) || token.Kind != TokenKind.Text || token.Value != text) { - throw new QueryParseException(text + " expected."); + throw new QueryParseException($"{text} expected."); } } @@ -83,7 +83,7 @@ protected void EatSingleCharacterToken(TokenKind kind) if (!TokenStack.TryPop(out Token token) || token.Kind != kind) { char ch = QueryTokenizer.SingleCharacterToTokenKinds.Single(pair => pair.Value == kind).Key; - throw new QueryParseException(ch + " expected."); + throw new QueryParseException($"{ch} expected."); } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs index feabc655ea..7d98110362 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs @@ -14,9 +14,9 @@ public class QueryStringParameterScopeParser : QueryExpressionParser private readonly Action _validateSingleFieldCallback; private ResourceContext _resourceContextInScope; - public QueryStringParameterScopeParser(IResourceContextProvider resourceContextProvider, FieldChainRequirements chainRequirements, + public QueryStringParameterScopeParser(IResourceGraph resourceGraph, FieldChainRequirements chainRequirements, Action validateSingleFieldCallback = null) - : base(resourceContextProvider) + : base(resourceGraph) { _chainRequirements = chainRequirements; _validateSingleFieldCallback = validateSingleFieldCallback; diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs index 71c86ed9ba..edb7a774f2 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs @@ -11,13 +11,13 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing /// internal sealed class ResourceFieldChainResolver { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public ResourceFieldChainResolver(IResourceContextProvider resourceContextProvider) + public ResourceFieldChainResolver(IResourceGraph resourceGraph) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } /// @@ -38,7 +38,7 @@ public IImmutableList ResolveToManyChain(ResourceContext validateCallback?.Invoke(relationship, nextResourceContext, path); chainBuilder.Add(relationship); - nextResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + nextResourceContext = _resourceGraph.GetResourceContext(relationship.RightType); } string lastName = publicNameParts[^1]; @@ -75,7 +75,7 @@ public IImmutableList ResolveRelationshipChain(ResourceC validateCallback?.Invoke(relationship, nextResourceContext, path); chainBuilder.Add(relationship); - nextResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + nextResourceContext = _resourceGraph.GetResourceContext(relationship.RightType); } return chainBuilder.ToImmutable(); @@ -103,7 +103,7 @@ public IImmutableList ResolveToOneChainEndingInAttribute validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); chainBuilder.Add(toOneRelationship); - nextResourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); + nextResourceContext = _resourceGraph.GetResourceContext(toOneRelationship.RightType); } string lastName = publicNameParts[^1]; @@ -139,7 +139,7 @@ public IImmutableList ResolveToOneChainEndingInToMany(Re validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); chainBuilder.Add(toOneRelationship); - nextResourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); + nextResourceContext = _resourceGraph.GetResourceContext(toOneRelationship.RightType); } string lastName = publicNameParts[^1]; @@ -176,7 +176,7 @@ public IImmutableList ResolveToOneChainEndingInAttribute validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); chainBuilder.Add(toOneRelationship); - nextResourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); + nextResourceContext = _resourceGraph.GetResourceContext(toOneRelationship.RightType); } string lastName = publicNameParts[^1]; @@ -197,7 +197,7 @@ public IImmutableList ResolveToOneChainEndingInAttribute private RelationshipAttribute GetRelationship(string publicName, ResourceContext resourceContext, string path) { - RelationshipAttribute relationship = resourceContext.Relationships.FirstOrDefault(nextRelationship => nextRelationship.PublicName == publicName); + RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(publicName); if (relationship == null) { @@ -239,7 +239,7 @@ private RelationshipAttribute GetToOneRelationship(string publicName, ResourceCo private AttrAttribute GetAttribute(string publicName, ResourceContext resourceContext, string path) { - AttrAttribute attribute = resourceContext.Attributes.FirstOrDefault(nextAttribute => nextAttribute.PublicName == publicName); + AttrAttribute attribute = resourceContext.TryGetAttributeByPublicName(publicName); if (attribute == null) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs index d9f0413935..4d588bacbb 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs @@ -14,9 +14,8 @@ public class SortParser : QueryExpressionParser private readonly Action _validateSingleFieldCallback; private ResourceContext _resourceContextInScope; - public SortParser(IResourceContextProvider resourceContextProvider, - Action validateSingleFieldCallback = null) - : base(resourceContextProvider) + public SortParser(IResourceGraph resourceGraph, Action validateSingleFieldCallback = null) + : base(resourceGraph) { _validateSingleFieldCallback = validateSingleFieldCallback; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs index dd45d1fe24..ea44dca7e5 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs @@ -14,9 +14,8 @@ public class SparseFieldSetParser : QueryExpressionParser private readonly Action _validateSingleFieldCallback; private ResourceContext _resourceContext; - public SparseFieldSetParser(IResourceContextProvider resourceContextProvider, - Action validateSingleFieldCallback = null) - : base(resourceContextProvider) + public SparseFieldSetParser(IResourceGraph resourceGraph, Action validateSingleFieldCallback = null) + : base(resourceGraph) { _validateSingleFieldCallback = validateSingleFieldCallback; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs index be9764f1ff..fec5356282 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs @@ -9,12 +9,12 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing [PublicAPI] public class SparseFieldTypeParser : QueryExpressionParser { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public SparseFieldTypeParser(IResourceContextProvider resourceContextProvider) - : base(resourceContextProvider) + public SparseFieldTypeParser(IResourceGraph resourceGraph) + : base(resourceGraph) { - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } public ResourceContext Parse(string source) @@ -56,7 +56,7 @@ private ResourceContext ParseResourceName() private ResourceContext GetResourceContext(string publicName) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(publicName); + ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(publicName); if (resourceContext == null) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index 236a3d80f6..6bc933cfc0 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -17,7 +17,7 @@ public class QueryLayerComposer : IQueryLayerComposer { private readonly CollectionConverter _collectionConverter = new(); private readonly IEnumerable _constraintProviders; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly IJsonApiOptions _options; private readonly IPaginationContext _paginationContext; @@ -25,12 +25,12 @@ public class QueryLayerComposer : IQueryLayerComposer private readonly IEvaluatedIncludeCache _evaluatedIncludeCache; private readonly SparseFieldSetCache _sparseFieldSetCache; - public QueryLayerComposer(IEnumerable constraintProviders, IResourceContextProvider resourceContextProvider, + public QueryLayerComposer(IEnumerable constraintProviders, IResourceGraph resourceGraph, IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiOptions options, IPaginationContext paginationContext, ITargetedFields targetedFields, IEvaluatedIncludeCache evaluatedIncludeCache) { ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(paginationContext, nameof(paginationContext)); @@ -38,7 +38,7 @@ public QueryLayerComposer(IEnumerable constraintProvid ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache)); _constraintProviders = constraintProviders; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _resourceDefinitionAccessor = resourceDefinitionAccessor; _options = options; _paginationContext = paginationContext; @@ -169,7 +169,7 @@ private IImmutableList ProcessIncludeSet(IImmutableLis // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(includeElement.Relationship.RightType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(includeElement.Relationship.RightType); bool isToManyRelationship = includeElement.Relationship is HasManyAttribute; var child = new QueryLayer(resourceContext) @@ -367,7 +367,7 @@ public QueryLayer ComposeForGetRelationshipRightIds(RelationshipAttribute relati ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); - ResourceContext rightResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + ResourceContext rightResourceContext = _resourceGraph.GetResourceContext(relationship.RightType); AttrAttribute rightIdAttribute = GetIdAttribute(rightResourceContext); object[] typedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); @@ -392,10 +392,10 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T ArgumentGuard.NotNull(hasManyRelationship, nameof(hasManyRelationship)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); - ResourceContext leftResourceContext = _resourceContextProvider.GetResourceContext(hasManyRelationship.LeftType); + ResourceContext leftResourceContext = _resourceGraph.GetResourceContext(hasManyRelationship.LeftType); AttrAttribute leftIdAttribute = GetIdAttribute(leftResourceContext); - ResourceContext rightResourceContext = _resourceContextProvider.GetResourceContext(hasManyRelationship.RightType); + ResourceContext rightResourceContext = _resourceGraph.GetResourceContext(hasManyRelationship.RightType); AttrAttribute rightIdAttribute = GetIdAttribute(rightResourceContext); object[] rightTypedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); @@ -493,7 +493,7 @@ protected virtual IDictionary GetProjectionF private static AttrAttribute GetIdAttribute(ResourceContext resourceContext) { - return resourceContext.Attributes.Single(attr => attr.Property.Name == nameof(Identifiable.Id)); + return resourceContext.GetAttributeByPropertyName(nameof(Identifiable.Id)); } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs index 7f90e334ad..081cf0be34 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs @@ -19,19 +19,18 @@ public class IncludeClauseBuilder : QueryClauseBuilder private readonly Expression _source; private readonly ResourceContext _resourceContext; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public IncludeClauseBuilder(Expression source, LambdaScope lambdaScope, ResourceContext resourceContext, - IResourceContextProvider resourceContextProvider) + public IncludeClauseBuilder(Expression source, LambdaScope lambdaScope, ResourceContext resourceContext, IResourceGraph resourceGraph) : base(lambdaScope) { ArgumentGuard.NotNull(source, nameof(source)); ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _source = source; _resourceContext = resourceContext; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } public Expression ApplyInclude(IncludeExpression include) @@ -60,9 +59,9 @@ private Expression ProcessRelationshipChain(ResourceFieldChainExpression chain, foreach (RelationshipAttribute relationship in chain.Fields.Cast()) { - path = path == null ? relationship.Property.Name : path + "." + relationship.Property.Name; + path = path == null ? relationship.Property.Name : $"{path}.{relationship.Property.Name}"; - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(relationship.RightType); result = ApplyEagerLoads(result, resourceContext.EagerLoads, path); } @@ -75,7 +74,7 @@ private Expression ApplyEagerLoads(Expression source, IEnumerable private readonly Type _extensionType; private readonly LambdaParameterNameFactory _nameFactory; private readonly IResourceFactory _resourceFactory; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; public SelectClauseBuilder(Expression source, LambdaScope lambdaScope, IModel entityModel, Type extensionType, LambdaParameterNameFactory nameFactory, - IResourceFactory resourceFactory, IResourceContextProvider resourceContextProvider) + IResourceFactory resourceFactory, IResourceGraph resourceGraph) : base(lambdaScope) { ArgumentGuard.NotNull(source, nameof(source)); @@ -39,14 +39,14 @@ public SelectClauseBuilder(Expression source, LambdaScope lambdaScope, IModel en ArgumentGuard.NotNull(extensionType, nameof(extensionType)); ArgumentGuard.NotNull(nameFactory, nameof(nameFactory)); ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _source = source; _entityModel = entityModel; _extensionType = extensionType; _nameFactory = nameFactory; _resourceFactory = resourceFactory; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } public Expression ApplySelect(IDictionary selectors, ResourceContext resourceContext) @@ -180,7 +180,7 @@ private Expression CreateCollectionInitializer(LambdaScope lambdaScope, Property { MemberExpression propertyExpression = Expression.Property(lambdaScope.Accessor, collectionProperty); - var builder = new QueryableBuilder(propertyExpression, elementType, typeof(Enumerable), _nameFactory, _resourceFactory, _resourceContextProvider, + var builder = new QueryableBuilder(propertyExpression, elementType, typeof(Enumerable), _nameFactory, _resourceFactory, _resourceGraph, _entityModel, lambdaScopeFactory); Expression layerExpression = builder.ApplyQuery(layer); @@ -221,7 +221,7 @@ public PropertySelector(PropertyInfo property, QueryLayer nextLayer = null) public override string ToString() { - return "Property: " + (NextLayer != null ? Property.Name + "..." : Property.Name); + return $"Property: {(NextLayer != null ? $"{Property.Name}..." : Property.Name)}"; } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs b/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs index 66c336d674..573b19e4a4 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs @@ -99,7 +99,7 @@ public IImmutableSet GetIdAttributeSetForRelationshipQuery(Resour { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); - AttrAttribute idAttribute = resourceContext.Attributes.Single(attr => attr.Property.Name == nameof(Identifiable.Id)); + AttrAttribute idAttribute = resourceContext.GetAttributeByPropertyName(nameof(Identifiable.Id)); var inputExpression = new SparseFieldSetExpression(ImmutableHashSet.Create(idAttribute)); // Intentionally not cached, as we are fetching ID only (ignoring any sparse fieldset that came from query string). @@ -146,10 +146,7 @@ private IImmutableSet GetResourceFields(ResourceContext fieldSetBuilder.Add(attribute); } - foreach (RelationshipAttribute relationship in resourceContext.Relationships) - { - fieldSetBuilder.Add(relationship); - } + fieldSetBuilder.AddRange(resourceContext.Relationships); return fieldSetBuilder.ToImmutable(); } diff --git a/src/JsonApiDotNetCore/Queries/QueryLayer.cs b/src/JsonApiDotNetCore/Queries/QueryLayer.cs index 1ea0f62733..9340181743 100644 --- a/src/JsonApiDotNetCore/Queries/QueryLayer.cs +++ b/src/JsonApiDotNetCore/Queries/QueryLayer.cs @@ -41,7 +41,7 @@ public override string ToString() private static void WriteLayer(IndentingStringWriter writer, QueryLayer layer, string prefix = null) { - writer.WriteLine(prefix + nameof(QueryLayer) + "<" + layer.ResourceContext.ResourceType.Name + ">"); + writer.WriteLine($"{prefix}{nameof(QueryLayer)}<{layer.ResourceContext.ResourceType.Name}>"); using (writer.Indent()) { @@ -79,7 +79,7 @@ private static void WriteLayer(IndentingStringWriter writer, QueryLayer layer, s } else { - WriteLayer(writer, nextLayer, field.PublicName + ": "); + WriteLayer(writer, nextLayer, $"{field.PublicName}: "); } } } diff --git a/src/JsonApiDotNetCore/QueryStrings/IDefaultsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/IDefaultsQueryStringParameterReader.cs deleted file mode 100644 index 176bf69bda..0000000000 --- a/src/JsonApiDotNetCore/QueryStrings/IDefaultsQueryStringParameterReader.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.QueryStrings -{ - /// - /// Reads the 'defaults' query string parameter. - /// - public interface IDefaultsQueryStringParameterReader : IQueryStringParameterReader - { - /// - /// Contains the effective value of default configuration and query string override, after parsing has occurred. - /// - DefaultValueHandling SerializerDefaultValueHandling { get; } - } -} diff --git a/src/JsonApiDotNetCore/QueryStrings/INullsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/INullsQueryStringParameterReader.cs deleted file mode 100644 index e1885925e5..0000000000 --- a/src/JsonApiDotNetCore/QueryStrings/INullsQueryStringParameterReader.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.QueryStrings -{ - /// - /// Reads the 'nulls' query string parameter. - /// - public interface INullsQueryStringParameterReader : IQueryStringParameterReader - { - /// - /// Contains the effective value of default configuration and query string override, after parsing has occurred. - /// - NullValueHandling SerializerNullValueHandling { get; } - } -} diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs deleted file mode 100644 index a58a16fdd3..0000000000 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs +++ /dev/null @@ -1,54 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers.Annotations; -using JsonApiDotNetCore.Errors; -using Microsoft.Extensions.Primitives; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.QueryStrings.Internal -{ - /// - [PublicAPI] - public class DefaultsQueryStringParameterReader : IDefaultsQueryStringParameterReader - { - private readonly IJsonApiOptions _options; - - /// - public DefaultValueHandling SerializerDefaultValueHandling { get; private set; } - - public DefaultsQueryStringParameterReader(IJsonApiOptions options) - { - ArgumentGuard.NotNull(options, nameof(options)); - - _options = options; - SerializerDefaultValueHandling = options.SerializerSettings.DefaultValueHandling; - } - - /// - public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) - { - ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - - return _options.AllowQueryStringOverrideForSerializerDefaultValueHandling && - !disableQueryStringAttribute.ContainsParameter(JsonApiQueryStringParameters.Defaults); - } - - /// - public virtual bool CanRead(string parameterName) - { - return parameterName == "defaults"; - } - - /// - public virtual void Read(string parameterName, StringValues parameterValue) - { - if (!bool.TryParse(parameterValue, out bool result)) - { - throw new InvalidQueryStringParameterException(parameterName, "The specified defaults is invalid.", - $"The value '{parameterValue}' must be 'true' or 'false'."); - } - - SerializerDefaultValueHandling = result ? DefaultValueHandling.Include : DefaultValueHandling.Ignore; - } - } -} diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs index 899363178b..354cb4b8ec 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs @@ -29,15 +29,15 @@ public class FilterQueryStringParameterReader : QueryStringParameterReader, IFil private string _lastParameterName; - public FilterQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, + public FilterQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IJsonApiOptions options) - : base(request, resourceContextProvider) + : base(request, resourceGraph) { ArgumentGuard.NotNull(options, nameof(options)); _options = options; - _scopeParser = new QueryStringParameterScopeParser(resourceContextProvider, FieldChainRequirements.EndsInToMany); - _filterParser = new FilterParser(resourceContextProvider, resourceFactory, ValidateSingleField); + _scopeParser = new QueryStringParameterScopeParser(resourceGraph, FieldChainRequirements.EndsInToMany); + _filterParser = new FilterParser(resourceGraph, resourceFactory, ValidateSingleField); } protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs index afdabbfbe9..2bed425170 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs @@ -21,13 +21,13 @@ public class IncludeQueryStringParameterReader : QueryStringParameterReader, IIn private IncludeExpression _includeExpression; private string _lastParameterName; - public IncludeQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IJsonApiOptions options) - : base(request, resourceContextProvider) + public IncludeQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph, IJsonApiOptions options) + : base(request, resourceGraph) { ArgumentGuard.NotNull(options, nameof(options)); _options = options; - _includeParser = new IncludeParser(resourceContextProvider, ValidateSingleRelationship); + _includeParser = new IncludeParser(resourceGraph, ValidateSingleRelationship); } protected void ValidateSingleRelationship(RelationshipAttribute relationship, ResourceContext resourceContext, string path) diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs index e65e52a8fd..d3cfaec888 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs @@ -82,7 +82,7 @@ public IEnumerable ExtractConditions(string parameterValue) if (parameterValue.StartsWith(InPrefix, StringComparison.Ordinal)) { string[] valueParts = parameterValue.Substring(InPrefix.Length).Split(","); - string valueList = "'" + string.Join("','", valueParts) + "'"; + string valueList = $"'{string.Join("','", valueParts)}'"; string expression = $"{Keywords.Any}({attributeName},{valueList})"; return (OutputParameterName, expression); @@ -91,7 +91,7 @@ public IEnumerable ExtractConditions(string parameterValue) if (parameterValue.StartsWith(NotInPrefix, StringComparison.Ordinal)) { string[] valueParts = parameterValue.Substring(NotInPrefix.Length).Split(","); - string valueList = "'" + string.Join("','", valueParts) + "'"; + string valueList = $"'{string.Join("','", valueParts)}'"; string expression = $"{Keywords.Not}({Keywords.Any}({attributeName},{valueList}))"; return (OutputParameterName, expression); diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs deleted file mode 100644 index aa40afbf25..0000000000 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs +++ /dev/null @@ -1,54 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers.Annotations; -using JsonApiDotNetCore.Errors; -using Microsoft.Extensions.Primitives; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.QueryStrings.Internal -{ - /// - [PublicAPI] - public class NullsQueryStringParameterReader : INullsQueryStringParameterReader - { - private readonly IJsonApiOptions _options; - - /// - public NullValueHandling SerializerNullValueHandling { get; private set; } - - public NullsQueryStringParameterReader(IJsonApiOptions options) - { - ArgumentGuard.NotNull(options, nameof(options)); - - _options = options; - SerializerNullValueHandling = options.SerializerSettings.NullValueHandling; - } - - /// - public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) - { - ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - - return _options.AllowQueryStringOverrideForSerializerNullValueHandling && - !disableQueryStringAttribute.ContainsParameter(JsonApiQueryStringParameters.Nulls); - } - - /// - public virtual bool CanRead(string parameterName) - { - return parameterName == "nulls"; - } - - /// - public virtual void Read(string parameterName, StringValues parameterValue) - { - if (!bool.TryParse(parameterValue, out bool result)) - { - throw new InvalidQueryStringParameterException(parameterName, "The specified nulls is invalid.", - $"The value '{parameterValue}' must be 'true' or 'false'."); - } - - SerializerNullValueHandling = result ? NullValueHandling.Include : NullValueHandling.Ignore; - } - } -} diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs index ae6d8c4e67..47c6ec595e 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs @@ -25,13 +25,13 @@ public class PaginationQueryStringParameterReader : QueryStringParameterReader, private PaginationQueryStringValueExpression _pageSizeConstraint; private PaginationQueryStringValueExpression _pageNumberConstraint; - public PaginationQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IJsonApiOptions options) - : base(request, resourceContextProvider) + public PaginationQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph, IJsonApiOptions options) + : base(request, resourceGraph) { ArgumentGuard.NotNull(options, nameof(options)); _options = options; - _paginationParser = new PaginationParser(resourceContextProvider); + _paginationParser = new PaginationParser(resourceGraph); } /// diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs index e6fb1226da..b026ae7587 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs @@ -9,18 +9,18 @@ namespace JsonApiDotNetCore.QueryStrings.Internal { public abstract class QueryStringParameterReader { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly bool _isCollectionRequest; protected ResourceContext RequestResource { get; } protected bool IsAtomicOperationsRequest { get; } - protected QueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider) + protected QueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _isCollectionRequest = request.IsCollection; RequestResource = request.SecondaryResource ?? request.PrimaryResource; IsAtomicOperationsRequest = request.Kind == EndpointKind.AtomicOperations; @@ -36,7 +36,7 @@ protected ResourceContext GetResourceContextForScope(ResourceFieldChainExpressio ResourceFieldAttribute lastField = scope.Fields[^1]; Type type = lastField is RelationshipAttribute relationship ? relationship.RightType : lastField.Property.PropertyType; - return _resourceContextProvider.GetResourceContext(type); + return _resourceGraph.GetResourceContext(type); } protected void AssertIsCollectionRequest() diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs index 68f9cc4a6c..28aabb53e5 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs @@ -68,8 +68,8 @@ public virtual void ReadAll(DisableQueryStringAttribute disableQueryStringAttrib else if (!_options.AllowUnknownQueryStringParameters) { throw new InvalidQueryStringParameterException(parameterName, "Unknown query string parameter.", - $"Query string parameter '{parameterName}' is unknown. " + - $"Set '{nameof(IJsonApiOptions.AllowUnknownQueryStringParameters)}' to 'true' in options to ignore unknown parameters."); + $"Query string parameter '{parameterName}' is unknown. Set '{nameof(IJsonApiOptions.AllowUnknownQueryStringParameters)}' " + + "to 'true' in options to ignore unknown parameters."); } } } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs index d96efbb4f5..e1ca5e0cd8 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs @@ -21,11 +21,11 @@ public class SortQueryStringParameterReader : QueryStringParameterReader, ISortQ private readonly List _constraints = new(); private string _lastParameterName; - public SortQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider) - : base(request, resourceContextProvider) + public SortQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph) + : base(request, resourceGraph) { - _scopeParser = new QueryStringParameterScopeParser(resourceContextProvider, FieldChainRequirements.EndsInToMany); - _sortParser = new SortParser(resourceContextProvider, ValidateSingleField); + _scopeParser = new QueryStringParameterScopeParser(resourceGraph, FieldChainRequirements.EndsInToMany); + _sortParser = new SortParser(resourceGraph, ValidateSingleField); } protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs index 08b8fa902f..096b31a7a1 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs @@ -30,11 +30,11 @@ public class SparseFieldSetQueryStringParameterReader : QueryStringParameterRead /// bool IQueryStringParameterReader.AllowEmptyValue => true; - public SparseFieldSetQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider) - : base(request, resourceContextProvider) + public SparseFieldSetQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph) + : base(request, resourceGraph) { - _sparseFieldTypeParser = new SparseFieldTypeParser(resourceContextProvider); - _sparseFieldSetParser = new SparseFieldSetParser(resourceContextProvider, ValidateSingleField); + _sparseFieldTypeParser = new SparseFieldTypeParser(resourceGraph); + _sparseFieldSetParser = new SparseFieldSetParser(resourceGraph, ValidateSingleField); } protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) @@ -92,7 +92,7 @@ private SparseFieldSetExpression GetSparseFieldSet(string parameterValue, Resour if (sparseFieldSet == null) { // We add ID on an incoming empty fieldset, so that callers can distinguish between no fieldset and an empty one. - AttrAttribute idAttribute = resourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Identifiable.Id)); + AttrAttribute idAttribute = resourceContext.GetAttributeByPropertyName(nameof(Identifiable.Id)); return new SparseFieldSetExpression(ImmutableHashSet.Create(idAttribute)); } diff --git a/src/JsonApiDotNetCore/QueryStrings/JsonApiQueryStringParameters.cs b/src/JsonApiDotNetCore/QueryStrings/JsonApiQueryStringParameters.cs index 95406014ef..9c220686ac 100644 --- a/src/JsonApiDotNetCore/QueryStrings/JsonApiQueryStringParameters.cs +++ b/src/JsonApiDotNetCore/QueryStrings/JsonApiQueryStringParameters.cs @@ -15,8 +15,6 @@ public enum JsonApiQueryStringParameters Include = 4, Page = 8, Fields = 16, - Nulls = 32, - Defaults = 64, - All = Filter | Sort | Include | Page | Fields | Nulls | Defaults + All = Filter | Sort | Include | Page | Fields } } diff --git a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs index c23ca04fd1..d1ccdb418d 100644 --- a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs @@ -18,17 +18,17 @@ namespace JsonApiDotNetCore.Repositories public class ResourceRepositoryAccessor : IResourceRepositoryAccessor { private readonly IServiceProvider _serviceProvider; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IJsonApiRequest _request; - public ResourceRepositoryAccessor(IServiceProvider serviceProvider, IResourceContextProvider resourceContextProvider, IJsonApiRequest request) + public ResourceRepositoryAccessor(IServiceProvider serviceProvider, IResourceGraph resourceGraph, IJsonApiRequest request) { ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(request, nameof(request)); _serviceProvider = serviceProvider; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _request = request; } @@ -124,7 +124,7 @@ public async Task RemoveFromToManyRelationshipAsync(TResource leftRes protected virtual object ResolveReadRepository(Type resourceType) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); if (resourceContext.IdentityType == typeof(int)) { @@ -149,7 +149,7 @@ private object GetWriteRepository(Type resourceType) { if (writeRepository is not IRepositorySupportsTransaction repository) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); throw new MissingTransactionSupportException(resourceContext.PublicName); } @@ -164,7 +164,7 @@ private object GetWriteRepository(Type resourceType) protected virtual object ResolveWriteRepository(Type resourceType) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); if (resourceContext.IdentityType == typeof(int)) { diff --git a/src/JsonApiDotNetCore/Resources/Internal/RuntimeTypeConverter.cs b/src/JsonApiDotNetCore/Resources/Internal/RuntimeTypeConverter.cs index b05acd5eba..f7aaad9192 100644 --- a/src/JsonApiDotNetCore/Resources/Internal/RuntimeTypeConverter.cs +++ b/src/JsonApiDotNetCore/Resources/Internal/RuntimeTypeConverter.cs @@ -16,7 +16,8 @@ public static object ConvertType(object value, Type type) { if (!CanContainNull(type)) { - throw new FormatException($"Failed to convert 'null' to type '{type.Name}'."); + string targetTypeName = type.GetFriendlyTypeName(); + throw new FormatException($"Failed to convert 'null' to type '{targetTypeName}'."); } return null; @@ -73,7 +74,10 @@ public static object ConvertType(object value, Type type) catch (Exception exception) when (exception is FormatException || exception is OverflowException || exception is InvalidCastException || exception is ArgumentException) { - throw new FormatException($"Failed to convert '{value}' of type '{runtimeType.Name}' to type '{type.Name}'.", exception); + string runtimeTypeName = runtimeType.GetFriendlyTypeName(); + string targetTypeName = type.GetFriendlyTypeName(); + + throw new FormatException($"Failed to convert '{value}' of type '{runtimeTypeName}' to type '{targetTypeName}'.", exception); } } diff --git a/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs index dc1aeafb91..b29fe33ef1 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; +using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Resources { @@ -11,22 +11,19 @@ namespace JsonApiDotNetCore.Resources public sealed class ResourceChangeTracker : IResourceChangeTracker where TResource : class, IIdentifiable { - private readonly IJsonApiOptions _options; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly ITargetedFields _targetedFields; private IDictionary _initiallyStoredAttributeValues; private IDictionary _requestAttributeValues; private IDictionary _finallyStoredAttributeValues; - public ResourceChangeTracker(IJsonApiOptions options, IResourceContextProvider resourceContextProvider, ITargetedFields targetedFields) + public ResourceChangeTracker(IResourceGraph resourceGraph, ITargetedFields targetedFields) { - ArgumentGuard.NotNull(options, nameof(options)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); - _options = options; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _targetedFields = targetedFields; } @@ -35,7 +32,7 @@ public void SetInitiallyStoredAttributeValues(TResource resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(); _initiallyStoredAttributeValues = CreateAttributeDictionary(resource, resourceContext.Attributes); } @@ -52,7 +49,7 @@ public void SetFinallyStoredAttributeValues(TResource resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(); _finallyStoredAttributeValues = CreateAttributeDictionary(resource, resourceContext.Attributes); } @@ -63,7 +60,7 @@ private IDictionary CreateAttributeDictionary(TResource resource foreach (AttrAttribute attribute in attributes) { object value = attribute.GetValue(resource); - string json = JsonConvert.SerializeObject(value, _options.SerializerSettings); + string json = JsonSerializer.Serialize(value); result.Add(attribute.PublicName, json); } diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs index 271f14ad5b..1923c33156 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -16,15 +16,15 @@ namespace JsonApiDotNetCore.Resources [PublicAPI] public class ResourceDefinitionAccessor : IResourceDefinitionAccessor { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IServiceProvider _serviceProvider; - public ResourceDefinitionAccessor(IResourceContextProvider resourceContextProvider, IServiceProvider serviceProvider) + public ResourceDefinitionAccessor(IResourceGraph resourceGraph, IServiceProvider serviceProvider) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _serviceProvider = serviceProvider; } @@ -194,7 +194,7 @@ public void OnSerialize(IIdentifiable resource) protected virtual object ResolveResourceDefinition(Type resourceType) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); if (resourceContext.IdentityType == typeof(int)) { diff --git a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs index 9f22dcbe3f..a7892755c5 100644 --- a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs @@ -59,7 +59,7 @@ public string Serialize(object content) return SerializeOperationsDocument(operations); } - if (content is ErrorDocument errorDocument) + if (content is Document errorDocument) { return SerializeErrorDocument(errorDocument); } @@ -69,12 +69,19 @@ public string Serialize(object content) private string SerializeOperationsDocument(IEnumerable operations) { - var document = new AtomicOperationsDocument + var document = new Document { Results = operations.Select(SerializeOperation).ToList(), Meta = _metaBuilder.Build() }; + SetApiVersion(document); + + return SerializeObject(document, _options.SerializerWriteOptions); + } + + private void SetApiVersion(Document document) + { if (_options.IncludeJsonApiVersion) { document.JsonApi = new JsonApiObject @@ -86,8 +93,6 @@ private string SerializeOperationsDocument(IEnumerable opera } }; } - - return SerializeObject(document, _options.SerializerSettings); } private AtomicResultObject SerializeOperation(OperationContainer operation) @@ -116,16 +121,15 @@ private AtomicResultObject SerializeOperation(OperationContainer operation) return new AtomicResultObject { - Data = resourceObject + Data = new SingleOrManyData(resourceObject) }; } - private string SerializeErrorDocument(ErrorDocument errorDocument) + private string SerializeErrorDocument(Document document) { - return SerializeObject(errorDocument, _options.SerializerSettings, serializer => - { - serializer.ApplyErrorSettings(); - }); + SetApiVersion(document); + + return SerializeObject(document, _options.SerializerWriteOptions); } } } diff --git a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs index b329392c9f..dfba94691c 100644 --- a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs @@ -1,18 +1,13 @@ -using System; using System.Collections; using System.Collections.Generic; -using System.IO; using System.Linq; +using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Resources.Internal; -using JsonApiDotNetCore.Serialization.Client.Internal; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace JsonApiDotNetCore.Serialization { @@ -25,18 +20,16 @@ public abstract class BaseDeserializer { private protected static readonly CollectionConverter CollectionConverter = new(); - protected IResourceContextProvider ResourceContextProvider { get; } + protected IResourceGraph ResourceGraph { get; } protected IResourceFactory ResourceFactory { get; } - protected Document Document { get; set; } - protected int? AtomicOperationIndex { get; set; } - protected BaseDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory) + protected BaseDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); - ResourceContextProvider = resourceContextProvider; + ResourceGraph = resourceGraph; ResourceFactory = resourceFactory; } @@ -45,7 +38,7 @@ protected BaseDeserializer(IResourceContextProvider resourceContextProvider, IRe /// depending on the type of deserializer. /// /// - /// See the implementation of this method in and for examples. + /// See the implementation of this method in for usage. /// /// /// The resource that was constructed from the document's body. @@ -56,33 +49,47 @@ protected BaseDeserializer(IResourceContextProvider resourceContextProvider, IRe /// /// Relationship data for . Is null when is not a . /// - protected abstract void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null); + protected abstract void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipObject data = null); - protected object DeserializeBody(string body) + protected Document DeserializeDocument(string body, JsonSerializerOptions serializerOptions) { ArgumentGuard.NotNullNorEmpty(body, nameof(body)); - using (CodeTimingSessionManager.Current.Measure("Newtonsoft.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages)) + try { - JToken bodyJToken = LoadJToken(body); - Document = bodyJToken.ToObject(); + using (CodeTimingSessionManager.Current.Measure("JsonSerializer.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages)) + { + return JsonSerializer.Deserialize(body, serializerOptions); + } } + catch (JsonException exception) + { + // JsonException.Path looks great for setting error.source.pointer, but unfortunately it is wrong in most cases. + // This is due to the use of custom converters, which are unable to interact with internal position tracking. + // https://github.com/dotnet/runtime/issues/50205#issuecomment-808401245 + throw new JsonApiSerializationException(null, exception.Message, exception); + } + } + + protected object DeserializeData(string body, JsonSerializerOptions serializerOptions) + { + Document document = DeserializeDocument(body, serializerOptions); - if (Document != null) + if (document != null) { - if (Document.IsManyData) + if (document.Data.ManyValue != null) { using (CodeTimingSessionManager.Current.Measure("Deserializer.Build (list)")) { - return Document.ManyData.Select(ParseResourceObject).ToHashSet(IdentifiableComparer.Instance); + return document.Data.ManyValue.Select(ParseResourceObject).ToHashSet(IdentifiableComparer.Instance); } } - if (Document.SingleData != null) + if (document.Data.SingleValue != null) { using (CodeTimingSessionManager.Current.Measure("Deserializer.Build (single)")) { - return ParseResourceObject(Document.SingleData); + return ParseResourceObject(document.Data.SingleValue); } } } @@ -102,8 +109,7 @@ protected object DeserializeBody(string body) /// /// Exposed attributes for . /// - protected IIdentifiable SetAttributes(IIdentifiable resource, IDictionary attributeValues, - IReadOnlyCollection attributes) + private IIdentifiable SetAttributes(IIdentifiable resource, IDictionary attributeValues, IReadOnlyCollection attributes) { ArgumentGuard.NotNull(resource, nameof(resource)); ArgumentGuard.NotNull(attributes, nameof(attributes)); @@ -123,8 +129,21 @@ protected IIdentifiable SetAttributes(IIdentifiable resource, IDictionary /// Exposed relationships for . /// - protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictionary relationshipValues, + private IIdentifiable SetRelationships(IIdentifiable resource, IDictionary relationshipValues, IReadOnlyCollection relationshipAttributes) { ArgumentGuard.NotNull(resource, nameof(resource)); @@ -157,9 +176,9 @@ protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictio foreach (RelationshipAttribute attr in relationshipAttributes) { - bool relationshipIsProvided = relationshipValues.TryGetValue(attr.PublicName, out RelationshipEntry relationshipData); + bool relationshipIsProvided = relationshipValues.TryGetValue(attr.PublicName, out RelationshipObject relationshipData); - if (!relationshipIsProvided || !relationshipData.IsPopulated) + if (!relationshipIsProvided || !relationshipData.Data.IsAssigned) { continue; } @@ -177,19 +196,6 @@ protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictio return resource; } -#pragma warning disable AV1130 // Return type in method signature should be a collection interface instead of a concrete type - protected JToken LoadJToken(string body) -#pragma warning restore AV1130 // Return type in method signature should be a collection interface instead of a concrete type - { - using JsonReader jsonReader = new JsonTextReader(new StringReader(body)) - { - // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/509 - DateParseHandling = DateParseHandling.None - }; - - return JToken.Load(jsonReader); - } - /// /// Creates an instance of the referenced type in and sets its attributes and relationships. /// @@ -223,7 +229,7 @@ protected IIdentifiable ParseResourceObject(ResourceObject data) protected ResourceContext GetExistingResourceContext(string publicName) { - ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(publicName); + ResourceContext resourceContext = ResourceGraph.TryGetResourceContext(publicName); if (resourceContext == null) { @@ -237,15 +243,15 @@ protected ResourceContext GetExistingResourceContext(string publicName) /// /// Sets a HasOne relationship on a parsed resource. /// - private void SetHasOneRelationship(IIdentifiable resource, HasOneAttribute hasOneRelationship, RelationshipEntry relationshipData) + private void SetHasOneRelationship(IIdentifiable resource, HasOneAttribute hasOneRelationship, RelationshipObject relationshipData) { - if (relationshipData.ManyData != null) + if (relationshipData.Data.ManyValue != null) { throw new JsonApiSerializationException("Expected single data element for to-one relationship.", $"Expected single data element for '{hasOneRelationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } - IIdentifiable rightResource = CreateRightResource(hasOneRelationship, relationshipData.SingleData); + IIdentifiable rightResource = CreateRightResource(hasOneRelationship, relationshipData.Data.SingleValue); hasOneRelationship.SetValue(resource, rightResource); // depending on if this base parser is used client-side or server-side, @@ -256,15 +262,15 @@ private void SetHasOneRelationship(IIdentifiable resource, HasOneAttribute hasOn /// /// Sets a HasMany relationship. /// - private void SetHasManyRelationship(IIdentifiable resource, HasManyAttribute hasManyRelationship, RelationshipEntry relationshipData) + private void SetHasManyRelationship(IIdentifiable resource, HasManyAttribute hasManyRelationship, RelationshipObject relationshipData) { - if (relationshipData.ManyData == null) + if (relationshipData.Data.ManyValue == null) { throw new JsonApiSerializationException("Expected data[] element for to-many relationship.", $"Expected data[] element for '{hasManyRelationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } - HashSet rightResources = relationshipData.ManyData.Select(rio => CreateRightResource(hasManyRelationship, rio)) + HashSet rightResources = relationshipData.Data.ManyValue.Select(rio => CreateRightResource(hasManyRelationship, rio)) .ToHashSet(IdentifiableComparer.Instance); IEnumerable convertedCollection = CollectionConverter.CopyToTypedCollection(rightResources, hasManyRelationship.Property.PropertyType); @@ -294,9 +300,9 @@ private IIdentifiable CreateRightResource(RelationshipAttribute relationship, Re } [AssertionMethod] - private void AssertHasType(ResourceIdentifierObject resourceIdentifierObject, RelationshipAttribute relationship) + private void AssertHasType(IResourceIdentity resourceIdentity, RelationshipAttribute relationship) { - if (resourceIdentifierObject.Type == null) + if (resourceIdentity.Type == null) { string details = relationship != null ? $"Expected 'type' element in '{relationship.PublicName}' relationship." @@ -332,11 +338,11 @@ private void AssertHasIdOrLid(ResourceIdentifierObject resourceIdentifierObject, } [AssertionMethod] - private void AssertHasNoLid(ResourceIdentifierObject resourceIdentifierObject) + private void AssertHasNoLid(IResourceIdentity resourceIdentityObject) { - if (resourceIdentifierObject.Lid != null) + if (resourceIdentityObject.Lid != null) { - throw new JsonApiSerializationException("Local IDs cannot be used at this endpoint.", null, atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException(null, "Local IDs cannot be used at this endpoint.", atomicOperationIndex: AtomicOperationIndex); } } @@ -349,23 +355,5 @@ private void AssertRightTypeIsCompatible(ResourceContext rightResourceContext, R atomicOperationIndex: AtomicOperationIndex); } } - - private object ConvertAttrValue(object newValue, Type targetType) - { - if (newValue is JContainer jObject) - { - // the attribute value is a complex type that needs additional deserialization - return DeserializeComplexType(jObject, targetType); - } - - // the attribute value is a native C# type. - object convertedValue = RuntimeTypeConverter.ConvertType(newValue, targetType); - return convertedValue; - } - - private object DeserializeComplexType(JContainer obj, Type targetType) - { - return obj.ToObject(targetType); - } } } diff --git a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs index 8519d20fd2..d096a4ea6c 100644 --- a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Text.Json; using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization { @@ -46,14 +45,11 @@ protected Document Build(IIdentifiable resource, IReadOnlyCollection(resourceObject) }; } @@ -80,33 +76,27 @@ protected Document Build(IReadOnlyCollection resources, IReadOnly using IDisposable _ = CodeTimingSessionManager.Current.Measure("Serializer.Build (list)"); - var data = new List(); + var resourceObjects = new List(); foreach (IIdentifiable resource in resources) { - data.Add(ResourceObjectBuilder.Build(resource, attributes, relationships)); + resourceObjects.Add(ResourceObjectBuilder.Build(resource, attributes, relationships)); } return new Document { - Data = data + Data = new SingleOrManyData(resourceObjects) }; } - protected string SerializeObject(object value, JsonSerializerSettings defaultSettings, Action changeSerializer = null) + protected string SerializeObject(object value, JsonSerializerOptions serializerOptions) { - ArgumentGuard.NotNull(defaultSettings, nameof(defaultSettings)); - - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Newtonsoft.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - - var serializer = JsonSerializer.CreateDefault(defaultSettings); - changeSerializer?.Invoke(serializer); + ArgumentGuard.NotNull(serializerOptions, nameof(serializerOptions)); - using var stringWriter = new StringWriter(); - using var jsonWriter = new JsonTextWriter(stringWriter); + using IDisposable _ = + CodeTimingSessionManager.Current.Measure("JsonSerializer.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - serializer.Serialize(jsonWriter, value); - return stringWriter.ToString(); + return JsonSerializer.Serialize(value, serializerOptions); } } } diff --git a/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs deleted file mode 100644 index 0fcaefd32a..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace JsonApiDotNetCore.Serialization.Building -{ - /// - /// Service that provides the server serializer with . - /// - public interface IResourceObjectBuilderSettingsProvider - { - /// - /// Gets the behavior for the serializer it is injected in. - /// - ResourceObjectBuilderSettings Get(); - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs index 4ba4a3f9f1..299c270f91 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs @@ -24,10 +24,10 @@ public class IncludedResourceObjectBuilder : ResourceObjectBuilder, IIncludedRes private readonly IRequestQueryStringAccessor _queryStringAccessor; private readonly SparseFieldSetCache _sparseFieldSetCache; - public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILinkBuilder linkBuilder, IResourceContextProvider resourceContextProvider, + public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILinkBuilder linkBuilder, IResourceGraph resourceGraph, IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, - IRequestQueryStringAccessor queryStringAccessor, IResourceObjectBuilderSettingsProvider settingsProvider) - : base(resourceContextProvider, settingsProvider.Get()) + IRequestQueryStringAccessor queryStringAccessor, IJsonApiOptions options) + : base(resourceGraph, options) { ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); @@ -35,7 +35,7 @@ public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILink ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); ArgumentGuard.NotNull(queryStringAccessor, nameof(queryStringAccessor)); - _included = new HashSet(ResourceIdentifierObjectComparer.Instance); + _included = new HashSet(ResourceIdentityComparer.Instance); _fieldsToSerialize = fieldsToSerialize; _linkBuilder = linkBuilder; _resourceDefinitionAccessor = resourceDefinitionAccessor; @@ -69,8 +69,8 @@ private void UpdateRelationships(ResourceObject resourceObject) { foreach (string relationshipName in resourceObject.Relationships.Keys) { - ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(resourceObject.Type); - RelationshipAttribute relationship = resourceContext.Relationships.Single(rel => rel.PublicName == relationshipName); + ResourceContext resourceContext = ResourceGraph.GetResourceContext(resourceObject.Type); + RelationshipAttribute relationship = resourceContext.GetRelationshipByPublicName(relationshipName); if (!IsRelationshipInSparseFieldSet(relationship)) { @@ -78,12 +78,12 @@ private void UpdateRelationships(ResourceObject resourceObject) } } - resourceObject.Relationships = PruneRelationshipEntries(resourceObject); + resourceObject.Relationships = PruneRelationshipObjects(resourceObject); } - private static IDictionary PruneRelationshipEntries(ResourceObject resourceObject) + private static IDictionary PruneRelationshipObjects(ResourceObject resourceObject) { - Dictionary pruned = resourceObject.Relationships.Where(pair => pair.Value.IsPopulated || pair.Value.Links != null) + Dictionary pruned = resourceObject.Relationships.Where(pair => pair.Value.Data.IsAssigned || pair.Value.Links != null) .ToDictionary(pair => pair.Key, pair => pair.Value); return !pruned.Any() ? null : pruned; @@ -91,7 +91,7 @@ private static IDictionary PruneRelationshipEntries(R private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) { - ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(relationship.LeftType); + ResourceContext resourceContext = ResourceGraph.GetResourceContext(relationship.LeftType); IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); return fieldSet.Contains(relationship); @@ -140,6 +140,11 @@ private void ProcessChain(object related, IList inclusion private void ProcessRelationship(IIdentifiable parent, IList inclusionChain) { + if (parent == null) + { + return; + } + ResourceObject resourceObject = TryGetBuiltResourceObjectFor(parent); if (resourceObject == null) @@ -159,18 +164,17 @@ private void ProcessRelationship(IIdentifiable parent, IList relationshipsObject = resourceObject.Relationships; + IDictionary relationshipsObject = resourceObject.Relationships; - // add the relationship entry in the relationship object. - if (!relationshipsObject.TryGetValue(nextRelationshipName, out RelationshipEntry relationshipEntry)) + if (!relationshipsObject.TryGetValue(nextRelationshipName, out RelationshipObject relationshipObject)) { - relationshipEntry = GetRelationshipData(nextRelationship, parent); - relationshipsObject[nextRelationshipName] = relationshipEntry; + relationshipObject = GetRelationshipData(nextRelationship, parent); + relationshipsObject[nextRelationshipName] = relationshipObject; } - relationshipEntry.Data = GetRelatedResourceLinkage(nextRelationship, parent); + relationshipObject.Data = GetRelatedResourceLinkage(nextRelationship, parent); - if (relationshipEntry.HasResource) + if (relationshipObject.Data.IsAssigned && relationshipObject.Data.Value != null) { // if the relationship is set, continue parsing the chain. object related = nextRelationship.GetValue(parent); @@ -186,14 +190,14 @@ private IList ShiftChain(IReadOnlyCollection - /// We only need an empty relationship object entry here. It will be populated in the ProcessRelationships method. + /// We only need an empty relationship object here. It will be populated in the ProcessRelationships method. /// - protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) + protected override RelationshipObject GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(resource, nameof(resource)); - return new RelationshipEntry + return new RelationshipObject { Links = _linkBuilder.GetRelationshipLinks(relationship, resource) }; @@ -202,7 +206,7 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r private ResourceObject TryGetBuiltResourceObjectFor(IIdentifiable resource) { Type resourceType = resource.GetType(); - ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = ResourceGraph.GetResourceContext(resourceType); return _included.SingleOrDefault(resourceObject => resourceObject.Type == resourceContext.PublicName && resourceObject.Id == resource.StringId); } diff --git a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs index 3ffcedbdfe..ad82acff5b 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs @@ -31,26 +31,25 @@ public class LinkBuilder : ILinkBuilder private readonly IJsonApiOptions _options; private readonly IJsonApiRequest _request; private readonly IPaginationContext _paginationContext; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IHttpContextAccessor _httpContextAccessor; private readonly LinkGenerator _linkGenerator; private readonly IControllerResourceMapping _controllerResourceMapping; - public LinkBuilder(IJsonApiOptions options, IJsonApiRequest request, IPaginationContext paginationContext, - IResourceContextProvider resourceContextProvider, IHttpContextAccessor httpContextAccessor, LinkGenerator linkGenerator, - IControllerResourceMapping controllerResourceMapping) + public LinkBuilder(IJsonApiOptions options, IJsonApiRequest request, IPaginationContext paginationContext, IResourceGraph resourceGraph, + IHttpContextAccessor httpContextAccessor, LinkGenerator linkGenerator, IControllerResourceMapping controllerResourceMapping) { ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(paginationContext, nameof(paginationContext)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(linkGenerator, nameof(linkGenerator)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); _options = options; _request = request; _paginationContext = paginationContext; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _httpContextAccessor = httpContextAccessor; _linkGenerator = linkGenerator; _controllerResourceMapping = controllerResourceMapping; @@ -173,7 +172,7 @@ private IImmutableList ParsePageSiz return ImmutableArray.Empty; } - var parser = new PaginationParser(_resourceContextProvider); + var parser = new PaginationParser(_resourceGraph); PaginationQueryStringValueExpression paginationExpression = parser.Parse(pageSizeParameterValue, requestResource); return paginationExpression.Elements; @@ -231,7 +230,7 @@ public ResourceLinks GetResourceLinks(string resourceName, string id) ArgumentGuard.NotNullNorEmpty(id, nameof(id)); var links = new ResourceLinks(); - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceName); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceName); if (_request.Kind != EndpointKind.Relationship && ShouldIncludeResourceLink(LinkTypes.Self, resourceContext)) { @@ -270,7 +269,7 @@ public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship ArgumentGuard.NotNull(leftResource, nameof(leftResource)); var links = new RelationshipLinks(); - ResourceContext leftResourceContext = _resourceContextProvider.GetResourceContext(leftResource.GetType()); + ResourceContext leftResourceContext = _resourceGraph.GetResourceContext(leftResource.GetType()); if (ShouldIncludeRelationshipLink(LinkTypes.Self, relationship, leftResourceContext)) { diff --git a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs index d741e6af26..dcddb2aa53 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs @@ -40,7 +40,11 @@ public IDictionary Build() { if (_paginationContext.TotalResourceCount != null) { - string key = _options.SerializerNamingStrategy.GetPropertyName("TotalResources", false); + const string keyName = "Total"; + + string key = _options.SerializerOptions.DictionaryKeyPolicy == null + ? keyName + : _options.SerializerOptions.DictionaryKeyPolicy.ConvertName(keyName); _meta.Add(key, _paginationContext.TotalResourceCount); } diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentifierObjectComparer.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentityComparer.cs similarity index 60% rename from src/JsonApiDotNetCore/Serialization/Building/ResourceIdentifierObjectComparer.cs rename to src/JsonApiDotNetCore/Serialization/Building/ResourceIdentityComparer.cs index 65760804c2..722008815e 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentifierObjectComparer.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentityComparer.cs @@ -4,15 +4,15 @@ namespace JsonApiDotNetCore.Serialization.Building { - internal sealed class ResourceIdentifierObjectComparer : IEqualityComparer + internal sealed class ResourceIdentityComparer : IEqualityComparer { - public static readonly ResourceIdentifierObjectComparer Instance = new(); + public static readonly ResourceIdentityComparer Instance = new(); - private ResourceIdentifierObjectComparer() + private ResourceIdentityComparer() { } - public bool Equals(ResourceIdentifierObject x, ResourceIdentifierObject y) + public bool Equals(IResourceIdentity x, IResourceIdentity y) { if (ReferenceEquals(x, y)) { @@ -27,7 +27,7 @@ public bool Equals(ResourceIdentifierObject x, ResourceIdentifierObject y) return x.Type == y.Type && x.Id == y.Id && x.Lid == y.Lid; } - public int GetHashCode(ResourceIdentifierObject obj) + public int GetHashCode(IResourceIdentity obj) { return HashCode.Combine(obj.Type, obj.Id, obj.Lid); } diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs index 787ce2a53b..6f78367108 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Resources.Internal; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization.Building { @@ -15,17 +15,17 @@ namespace JsonApiDotNetCore.Serialization.Building public class ResourceObjectBuilder : IResourceObjectBuilder { private static readonly CollectionConverter CollectionConverter = new(); + private readonly IJsonApiOptions _options; - private readonly ResourceObjectBuilderSettings _settings; - protected IResourceContextProvider ResourceContextProvider { get; } + protected IResourceGraph ResourceGraph { get; } - public ResourceObjectBuilder(IResourceContextProvider resourceContextProvider, ResourceObjectBuilderSettings settings) + public ResourceObjectBuilder(IResourceGraph resourceGraph, IJsonApiOptions options) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); - ArgumentGuard.NotNull(settings, nameof(settings)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(options, nameof(options)); - ResourceContextProvider = resourceContextProvider; - _settings = settings; + ResourceGraph = resourceGraph; + _options = options; } /// @@ -34,7 +34,7 @@ public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection< { ArgumentGuard.NotNull(resource, nameof(resource)); - ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(resource.GetType()); + ResourceContext resourceContext = ResourceGraph.GetResourceContext(resource.GetType()); // populating the top-level "type" and "id" members. var resourceObject = new ResourceObject @@ -64,25 +64,24 @@ public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection< } /// - /// Builds the entries of the "relationships objects". The default behavior is to just construct a resource linkage with - /// the "data" field populated with "single" or "many" data. Depending on the requirements of the implementation (server or client serializer), this may - /// be overridden. + /// Builds a . The default behavior is to just construct a resource linkage with the "data" field populated with + /// "single" or "many" data. /// - protected virtual RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) + protected virtual RelationshipObject GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(resource, nameof(resource)); - return new RelationshipEntry + return new RelationshipObject { Data = GetRelatedResourceLinkage(relationship, resource) }; } /// - /// Gets the value for the property. + /// Gets the value for the data property. /// - protected object GetRelatedResourceLinkage(RelationshipAttribute relationship, IIdentifiable resource) + protected SingleOrManyData GetRelatedResourceLinkage(RelationshipAttribute relationship, IIdentifiable resource) { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(resource, nameof(resource)); @@ -95,22 +94,17 @@ protected object GetRelatedResourceLinkage(RelationshipAttribute relationship, I /// /// Builds a for a HasOne relationship. /// - private ResourceIdentifierObject GetRelatedResourceLinkageForHasOne(HasOneAttribute relationship, IIdentifiable resource) + private SingleOrManyData GetRelatedResourceLinkageForHasOne(HasOneAttribute relationship, IIdentifiable resource) { var relatedResource = (IIdentifiable)relationship.GetValue(resource); - - if (relatedResource != null) - { - return GetResourceIdentifier(relatedResource); - } - - return null; + ResourceIdentifierObject resourceIdentifierObject = relatedResource != null ? GetResourceIdentifier(relatedResource) : null; + return new SingleOrManyData(resourceIdentifierObject); } /// /// Builds the s for a HasMany relationship. /// - private IList GetRelatedResourceLinkageForHasMany(HasManyAttribute relationship, IIdentifiable resource) + private SingleOrManyData GetRelatedResourceLinkageForHasMany(HasManyAttribute relationship, IIdentifiable resource) { object value = relationship.GetValue(resource); ICollection relatedResources = CollectionConverter.ExtractResources(value); @@ -125,7 +119,7 @@ private IList GetRelatedResourceLinkageForHasMany(HasM } } - return manyData; + return new SingleOrManyData(manyData); } /// @@ -133,7 +127,7 @@ private IList GetRelatedResourceLinkageForHasMany(HasM /// private ResourceIdentifierObject GetResourceIdentifier(IIdentifiable resource) { - string publicName = ResourceContextProvider.GetResourceContext(resource.GetType()).PublicName; + string publicName = ResourceGraph.GetResourceContext(resource.GetType()).PublicName; return new ResourceIdentifierObject { @@ -149,11 +143,11 @@ private void ProcessRelationships(IIdentifiable resource, IEnumerable()).Add(rel.PublicName, relData); + (ro.Relationships ??= new Dictionary()).Add(rel.PublicName, relData); } } } @@ -169,15 +163,15 @@ private void ProcessAttributes(IIdentifiable resource, IEnumerable - /// Options used to configure how fields of a model get serialized into a JSON:API . - /// - [PublicAPI] - public sealed class ResourceObjectBuilderSettings - { - public NullValueHandling SerializerNullValueHandling { get; } - public DefaultValueHandling SerializerDefaultValueHandling { get; } - - public ResourceObjectBuilderSettings(NullValueHandling serializerNullValueHandling = NullValueHandling.Include, - DefaultValueHandling serializerDefaultValueHandling = DefaultValueHandling.Include) - { - SerializerNullValueHandling = serializerNullValueHandling; - SerializerDefaultValueHandling = serializerDefaultValueHandling; - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs deleted file mode 100644 index 04a5128aed..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.QueryStrings; - -namespace JsonApiDotNetCore.Serialization.Building -{ - /// - /// This implementation of the behavior provider reads the defaults/nulls query string parameters that can, if provided, override the settings in - /// . - /// - public sealed class ResourceObjectBuilderSettingsProvider : IResourceObjectBuilderSettingsProvider - { - private readonly IDefaultsQueryStringParameterReader _defaultsReader; - private readonly INullsQueryStringParameterReader _nullsReader; - - public ResourceObjectBuilderSettingsProvider(IDefaultsQueryStringParameterReader defaultsReader, INullsQueryStringParameterReader nullsReader) - { - ArgumentGuard.NotNull(defaultsReader, nameof(defaultsReader)); - ArgumentGuard.NotNull(nullsReader, nameof(nullsReader)); - - _defaultsReader = defaultsReader; - _nullsReader = nullsReader; - } - - /// - public ResourceObjectBuilderSettings Get() - { - return new(_nullsReader.SerializerNullValueHandling, _defaultsReader.SerializerDefaultValueHandling); - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs index e23eef6a82..7138b6a12b 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs @@ -26,10 +26,9 @@ public class ResponseResourceObjectBuilder : ResourceObjectBuilder private RelationshipAttribute _requestRelationship; public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, - IEnumerable constraintProviders, IResourceContextProvider resourceContextProvider, - IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectBuilderSettingsProvider settingsProvider, - IEvaluatedIncludeCache evaluatedIncludeCache) - : base(resourceContextProvider, settingsProvider.Get()) + IEnumerable constraintProviders, IResourceGraph resourceGraph, IResourceDefinitionAccessor resourceDefinitionAccessor, + IJsonApiOptions options, IEvaluatedIncludeCache evaluatedIncludeCache) + : base(resourceGraph, options) { ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); ArgumentGuard.NotNull(includedBuilder, nameof(includedBuilder)); @@ -44,7 +43,7 @@ public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IIncludedResource _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); } - public RelationshipEntry Build(IIdentifiable resource, RelationshipAttribute requestRelationship) + public RelationshipObject Build(IIdentifiable resource, RelationshipAttribute requestRelationship) { ArgumentGuard.NotNull(resource, nameof(resource)); ArgumentGuard.NotNull(requestRelationship, nameof(requestRelationship)); @@ -65,23 +64,24 @@ public override ResourceObject Build(IIdentifiable resource, IReadOnlyCollection } /// - /// Builds the values of the relationships object on a resource object. The server serializer only populates the "data" member when the relationship is - /// included, and adds links unless these are turned off. This means that if a relationship is not included and links are turned off, the entry would be - /// completely empty, ie { }, which is not conform JSON:API spec. In that case we return null which will omit the entry from the output. + /// Builds a for the specified relationship on a resource. The serializer only populates the "data" member when the + /// relationship is included, and adds links unless these are turned off. This means that if a relationship is not included and links are turned off, the + /// object would be completely empty, ie { }, which is not conform JSON:API spec. In that case we return null, which will omit the object from the + /// output. /// - protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) + protected override RelationshipObject GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(resource, nameof(resource)); - RelationshipEntry relationshipEntry = null; + RelationshipObject relationshipObject = null; IReadOnlyCollection> relationshipChains = GetInclusionChainsStartingWith(relationship); if (Equals(relationship, _requestRelationship) || relationshipChains.Any()) { - relationshipEntry = base.GetRelationshipData(relationship, resource); + relationshipObject = base.GetRelationshipData(relationship, resource); - if (relationshipChains.Any() && relationshipEntry.HasResource) + if (relationshipChains.Any() && relationshipObject.Data.Value != null) { foreach (IReadOnlyCollection chain in relationshipChains) { @@ -100,19 +100,19 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r if (links != null) { - // if relationshipLinks should be built for this entry, populate the "links" field. - relationshipEntry ??= new RelationshipEntry(); - relationshipEntry.Links = links; + // if relationshipLinks should be built, populate the "links" field. + relationshipObject ??= new RelationshipObject(); + relationshipObject.Links = links; } - // if neither "links" nor "data" was populated, return null, which will omit this entry from the output. + // if neither "links" nor "data" was populated, return null, which will omit this object from the output. // (see the NullValueHandling settings on ) - return relationshipEntry; + return relationshipObject; } private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) { - ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(relationship.LeftType); + ResourceContext resourceContext = ResourceGraph.GetResourceContext(relationship.LeftType); IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); return fieldSet.Contains(relationship); diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs deleted file mode 100644 index f1051dfd47..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Base class for "single data" and "many data" deserialized responses. - /// - [PublicAPI] - public abstract class DeserializedResponseBase - { - public TopLevelLinks Links { get; set; } - public IDictionary Meta { get; set; } - public object Errors { get; set; } - public object JsonApi { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs deleted file mode 100644 index c1252c06ad..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Interface for client serializer that can be used to register with the DI container, for usage in custom services or repositories. - /// - [PublicAPI] - public interface IRequestSerializer - { - /// - /// Sets the attributes that will be included in the serialized request body. You can use - /// to conveniently access the desired instances. - /// - public IReadOnlyCollection AttributesToSerialize { get; set; } - - /// - /// Sets the relationships that will be included in the serialized request body. You can use - /// to conveniently access the desired instances. - /// - public IReadOnlyCollection RelationshipsToSerialize { get; set; } - - /// - /// Creates and serializes a document for a single resource. - /// - /// - /// The serialized content - /// - string Serialize(IIdentifiable resource); - - /// - /// Creates and serializes a document for a collection of resources. - /// - /// - /// The serialized content - /// - string Serialize(IReadOnlyCollection resources); - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs deleted file mode 100644 index 0a41306523..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs +++ /dev/null @@ -1,37 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Client deserializer. Currently not used internally in JsonApiDotNetCore, except for in the tests. Exposed publicly to make testing easier or to - /// implement server-to-server communication. - /// - [PublicAPI] - public interface IResponseDeserializer - { - /// - /// Deserializes a response with a single resource (or null) as data. - /// - /// - /// The type of the resources in the primary data. - /// - /// - /// The JSON to be deserialized. - /// - SingleResponse DeserializeSingle(string body) - where TResource : class, IIdentifiable; - - /// - /// Deserializes a response with an (empty) collection of resources as data. - /// - /// - /// The type of the resources in the primary data. - /// - /// - /// The JSON to be deserialized. - /// - ManyResponse DeserializeMany(string body) - where TResource : class, IIdentifiable; - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs deleted file mode 100644 index 659e33d0dd..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Represents a deserialized document with "many data". - /// - /// - /// Type of the resource(s) in the primary data. - /// - [PublicAPI] - public sealed class ManyResponse : DeserializedResponseBase - where TResource : class, IIdentifiable - { - public IReadOnlyCollection Data { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs deleted file mode 100644 index e0910b3a58..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Client serializer implementation of . - /// - [PublicAPI] - public class RequestSerializer : BaseSerializer, IRequestSerializer - { - private readonly IResourceGraph _resourceGraph; - private readonly JsonSerializerSettings _jsonSerializerSettings = new(); - private Type _currentTargetedResource; - - /// - public IReadOnlyCollection AttributesToSerialize { get; set; } - - /// - public IReadOnlyCollection RelationshipsToSerialize { get; set; } - - public RequestSerializer(IResourceGraph resourceGraph, IResourceObjectBuilder resourceObjectBuilder) - : base(resourceObjectBuilder) - { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - - _resourceGraph = resourceGraph; - } - - /// - public string Serialize(IIdentifiable resource) - { - if (resource == null) - { - Document empty = Build((IIdentifiable)null, Array.Empty(), Array.Empty()); - return SerializeObject(empty, _jsonSerializerSettings); - } - - _currentTargetedResource = resource.GetType(); - Document document = Build(resource, GetAttributesToSerialize(resource), RelationshipsToSerialize); - _currentTargetedResource = null; - - return SerializeObject(document, _jsonSerializerSettings); - } - - /// - public string Serialize(IReadOnlyCollection resources) - { - ArgumentGuard.NotNull(resources, nameof(resources)); - - IIdentifiable firstResource = resources.FirstOrDefault(); - - Document document; - - if (firstResource == null) - { - document = Build(resources, Array.Empty(), Array.Empty()); - } - else - { - _currentTargetedResource = firstResource.GetType(); - IReadOnlyCollection attributes = GetAttributesToSerialize(firstResource); - - document = Build(resources, attributes, RelationshipsToSerialize); - _currentTargetedResource = null; - } - - return SerializeObject(document, _jsonSerializerSettings); - } - - /// - /// By default, the client serializer includes all attributes in the result, unless a list of allowed attributes was supplied using the - /// method. For any related resources, attributes are never exposed. - /// - private IReadOnlyCollection GetAttributesToSerialize(IIdentifiable resource) - { - Type currentResourceType = resource.GetType(); - - if (_currentTargetedResource != currentResourceType) - { - // We're dealing with a relationship that is being serialized, for which - // we never want to include any attributes in the request body. - return new List(); - } - - if (AttributesToSerialize == null) - { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(currentResourceType); - return resourceContext.Attributes; - } - - return AttributesToSerialize; - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs deleted file mode 100644 index a08c3655fd..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Client deserializer implementation of the . - /// - [PublicAPI] - public class ResponseDeserializer : BaseDeserializer, IResponseDeserializer - { - public ResponseDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory) - : base(resourceContextProvider, resourceFactory) - { - } - - /// - public SingleResponse DeserializeSingle(string body) - where TResource : class, IIdentifiable - { - ArgumentGuard.NotNullNorEmpty(body, nameof(body)); - - object resource = DeserializeBody(body); - - return new SingleResponse - { - Links = Document.Links, - Meta = Document.Meta, - Data = (TResource)resource, - JsonApi = null, - Errors = null - }; - } - - /// - public ManyResponse DeserializeMany(string body) - where TResource : class, IIdentifiable - { - ArgumentGuard.NotNullNorEmpty(body, nameof(body)); - - object resources = DeserializeBody(body); - - return new ManyResponse - { - Links = Document.Links, - Meta = Document.Meta, - Data = ((ICollection)resources)?.Cast().ToArray(), - JsonApi = null, - Errors = null - }; - } - - /// - /// Additional processing required for client deserialization, responsible for parsing the property. When a relationship - /// value is parsed, it goes through the included list to set its attributes and relationships. - /// - /// - /// The resource that was constructed from the document's body. - /// - /// - /// The metadata for the exposed field. - /// - /// - /// Relationship data for . Is null when is not a . - /// - protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) - { - ArgumentGuard.NotNull(resource, nameof(resource)); - ArgumentGuard.NotNull(field, nameof(field)); - - // Client deserializers do not need additional processing for attributes. - if (field is AttrAttribute) - { - return; - } - - // if the included property is empty or absent, there is no additional data to be parsed. - if (Document.Included.IsNullOrEmpty()) - { - return; - } - - if (data != null) - { - if (field is HasOneAttribute hasOneAttr) - { - // add attributes and relationships of a parsed HasOne relationship - ResourceIdentifierObject rio = data.SingleData; - hasOneAttr.SetValue(resource, rio == null ? null : ParseIncludedRelationship(rio)); - } - else if (field is HasManyAttribute hasManyAttr) - { - // add attributes and relationships of a parsed HasMany relationship - IEnumerable items = data.ManyData.Select(ParseIncludedRelationship); - IEnumerable values = CollectionConverter.CopyToTypedCollection(items, hasManyAttr.Property.PropertyType); - hasManyAttr.SetValue(resource, values); - } - } - } - - /// - /// Searches for and parses the included relationship. - /// - private IIdentifiable ParseIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier) - { - ResourceContext relatedResourceContext = ResourceContextProvider.GetResourceContext(relatedResourceIdentifier.Type); - - if (relatedResourceContext == null) - { - throw new InvalidOperationException($"Included type '{relatedResourceIdentifier.Type}' is not a registered JSON:API resource."); - } - - IIdentifiable relatedInstance = ResourceFactory.CreateInstance(relatedResourceContext.ResourceType); - relatedInstance.StringId = relatedResourceIdentifier.Id; - - ResourceObject includedResource = GetLinkedResource(relatedResourceIdentifier); - - if (includedResource != null) - { - SetAttributes(relatedInstance, includedResource.Attributes, relatedResourceContext.Attributes); - SetRelationships(relatedInstance, includedResource.Relationships, relatedResourceContext.Relationships); - } - - return relatedInstance; - } - - private ResourceObject GetLinkedResource(ResourceIdentifierObject relatedResourceIdentifier) - { - try - { - return Document.Included.SingleOrDefault(resourceObject => - resourceObject.Type == relatedResourceIdentifier.Type && resourceObject.Id == relatedResourceIdentifier.Id); - } - catch (InvalidOperationException exception) - { - throw new InvalidOperationException( - "A compound document MUST NOT include more than one resource object for each type and ID pair." + - $"The duplicate pair was '{relatedResourceIdentifier.Type}, {relatedResourceIdentifier.Id}'", exception); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs deleted file mode 100644 index 3359cafae6..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Represents a deserialized document with "single data". - /// - /// - /// Type of the resource in the primary data. - /// - [PublicAPI] - public sealed class SingleResponse : DeserializedResponseBase - where TResource : class, IIdentifiable - { - public TResource Data { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs b/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs index 88648a70d2..bc1a7f3e49 100644 --- a/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs +++ b/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs @@ -18,7 +18,7 @@ public ETagGenerator(IFingerprintGenerator fingerprintGenerator) public EntityTagHeaderValue Generate(string requestUrl, string responseBody) { string fingerprint = _fingerprintGenerator.Generate(ArrayFactory.Create(requestUrl, responseBody)); - string eTagValue = "\"" + fingerprint + "\""; + string eTagValue = $"\"{fingerprint}\""; return EntityTagHeaderValue.Parse(eTagValue); } diff --git a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs index d4ace8450d..e19ff666d3 100644 --- a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs +++ b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs @@ -16,20 +16,20 @@ namespace JsonApiDotNetCore.Serialization [PublicAPI] public class FieldsToSerialize : IFieldsToSerialize { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IJsonApiRequest _request; private readonly SparseFieldSetCache _sparseFieldSetCache; /// public bool ShouldSerialize => _request.Kind != EndpointKind.Relationship; - public FieldsToSerialize(IResourceContextProvider resourceContextProvider, IEnumerable constraintProviders, + public FieldsToSerialize(IResourceGraph resourceGraph, IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiRequest request) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(request, nameof(request)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _request = request; _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); } @@ -44,7 +44,7 @@ public IReadOnlyCollection GetAttributes(Type resourceType) return Array.Empty(); } - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); return SortAttributesInDeclarationOrder(fieldSet, resourceContext).ToArray(); @@ -76,7 +76,7 @@ public IReadOnlyCollection GetRelationships(Type resource return Array.Empty(); } - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); return resourceContext.Relationships; } diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs index 3c363cbd4b..ea392575b9 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs @@ -8,8 +8,7 @@ namespace JsonApiDotNetCore.Serialization public interface IJsonApiDeserializer { /// - /// Deserializes JSON into a or and constructs resources from - /// . + /// Deserializes JSON into a and constructs resources from the 'data' element. /// /// /// The JSON to be deserialized. diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index d9e861d4a2..af634f9876 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -27,20 +27,19 @@ public class JsonApiReader : IJsonApiReader { private readonly IJsonApiDeserializer _deserializer; private readonly IJsonApiRequest _request; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly TraceLogWriter _traceWriter; - public JsonApiReader(IJsonApiDeserializer deserializer, IJsonApiRequest request, IResourceContextProvider resourceContextProvider, - ILoggerFactory loggerFactory) + public JsonApiReader(IJsonApiDeserializer deserializer, IJsonApiRequest request, IResourceGraph resourceGraph, ILoggerFactory loggerFactory) { ArgumentGuard.NotNull(deserializer, nameof(deserializer)); ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); _deserializer = deserializer; _request = request; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _traceWriter = new TraceLogWriter(loggerFactory); } @@ -107,8 +106,9 @@ private InvalidRequestBodyException ToInvalidRequestBodyException(JsonApiSeriali if (exception.AtomicOperationIndex != null) { - foreach (Error error in requestException.Errors) + foreach (ErrorObject error in requestException.Errors) { + error.Source ??= new ErrorSource(); error.Source.Pointer = $"/atomic:operations[{exception.AtomicOperationIndex}]"; } } @@ -149,7 +149,7 @@ private static void AssertHasRequestBody(object model, string body) { if (model == null && string.IsNullOrWhiteSpace(body)) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Missing request body." }); @@ -171,8 +171,8 @@ private void ValidateIncomingResourceType(object model, HttpRequest httpRequest) { if (!endpointResourceType.IsAssignableFrom(bodyResourceType)) { - ResourceContext resourceFromEndpoint = _resourceContextProvider.GetResourceContext(endpointResourceType); - ResourceContext resourceFromBody = _resourceContextProvider.GetResourceContext(bodyResourceType); + ResourceContext resourceFromEndpoint = _resourceGraph.GetResourceContext(endpointResourceType); + ResourceContext resourceFromBody = _resourceGraph.GetResourceContext(bodyResourceType); throw new ResourceTypeMismatchException(new HttpMethod(httpRequest.Method), httpRequest.Path, resourceFromEndpoint, resourceFromBody); } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs index 08c81da0f5..5ca93865c7 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Text; @@ -64,10 +65,10 @@ public async Task WriteAsync(OutputFormatterWriteContext context) catch (Exception exception) #pragma warning restore AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException { - ErrorDocument errorDocument = _exceptionHandler.HandleException(exception); - responseContent = _serializer.Serialize(errorDocument); + Document document = _exceptionHandler.HandleException(exception); + responseContent = _serializer.Serialize(document); - response.StatusCode = (int)errorDocument.GetErrorStatusCode(); + response.StatusCode = (int)document.GetErrorStatusCode(); } bool hasMatchingETag = SetETagResponseHeader(request, response, responseContent); @@ -129,14 +130,20 @@ private bool IsSuccessStatusCode(HttpStatusCode statusCode) private static object WrapErrors(object contextObject) { - if (contextObject is IEnumerable errors) + if (contextObject is IEnumerable errors) { - return new ErrorDocument(errors); + return new Document + { + Errors = errors.ToList() + }; } - if (contextObject is Error error) + if (contextObject is ErrorObject error) { - return new ErrorDocument(error); + return new Document + { + Errors = error.AsList() + }; } return contextObject; diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs new file mode 100644 index 0000000000..4f0758fff0 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.JsonConverters +{ + /// + /// Converts to/from JSON. + /// + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class ResourceObjectConverter : JsonObjectConverter + { + private static readonly JsonEncodedText TypeText = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText IdText = JsonEncodedText.Encode("id"); + private static readonly JsonEncodedText LidText = JsonEncodedText.Encode("lid"); + private static readonly JsonEncodedText MetaText = JsonEncodedText.Encode("meta"); + private static readonly JsonEncodedText AttributesText = JsonEncodedText.Encode("attributes"); + private static readonly JsonEncodedText RelationshipsText = JsonEncodedText.Encode("relationships"); + private static readonly JsonEncodedText LinksText = JsonEncodedText.Encode("links"); + + private readonly IResourceGraph _resourceGraph; + + public ResourceObjectConverter(IResourceGraph resourceGraph) + { + _resourceGraph = resourceGraph; + } + + /// + /// Resolves the resource type and attributes against the resource graph. Because attribute values in are typed as + /// , we must lookup and supply the target type to the serializer. + /// + public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Inside a JsonConverter there is no way to know where in the JSON object tree we are. And the serializer is unable to provide + // the correct position either. So we avoid an exception on missing/invalid 'type' element and postpone producing an error response + // to the post-processing phase. + + var resourceObject = new ResourceObject + { + // The 'attributes' element may occur before 'type', but we need to know the resource type before we can deserialize attributes + // into their corresponding CLR types. + Type = TryPeekType(ref reader) + }; + + ResourceContext resourceContext = resourceObject.Type != null ? _resourceGraph.TryGetResourceContext(resourceObject.Type) : null; + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.EndObject: + { + return resourceObject; + } + case JsonTokenType.PropertyName: + { + string propertyName = reader.GetString(); + reader.Read(); + + switch (propertyName) + { + case "id": + { + if (reader.TokenType != JsonTokenType.String) + { + // Newtonsoft.Json used to auto-convert number to strings, while System.Text.Json does not. This is so likely + // to hit users during upgrade that we special-case for this and produce a helpful error message. + var jsonElement = ReadSubTree(ref reader, options); + throw new JsonException($"Failed to convert ID '{jsonElement}' of type '{jsonElement.ValueKind}' to type 'String'."); + } + + resourceObject.Id = reader.GetString(); + break; + } + case "lid": + { + resourceObject.Lid = reader.GetString(); + break; + } + case "attributes": + { + if (resourceContext != null) + { + resourceObject.Attributes = ReadAttributes(ref reader, options, resourceContext); + } + else + { + reader.Skip(); + } + + break; + } + case "relationships": + { + resourceObject.Relationships = ReadSubTree>(ref reader, options); + break; + } + case "links": + { + resourceObject.Links = ReadSubTree(ref reader, options); + break; + } + case "meta": + { + resourceObject.Meta = ReadSubTree>(ref reader, options); + break; + } + default: + { + reader.Skip(); + break; + } + } + + break; + } + } + } + + throw GetEndOfStreamError(); + } + + private static string TryPeekType(ref Utf8JsonReader reader) + { + // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-5-0#an-alternative-way-to-do-polymorphic-deserialization + Utf8JsonReader readerClone = reader; + + while (readerClone.Read()) + { + if (readerClone.TokenType == JsonTokenType.PropertyName) + { + string propertyName = readerClone.GetString(); + readerClone.Read(); + + switch (propertyName) + { + case "type": + { + return readerClone.GetString(); + } + default: + { + readerClone.Skip(); + break; + } + } + } + } + + return null; + } + + private static IDictionary ReadAttributes(ref Utf8JsonReader reader, JsonSerializerOptions options, ResourceContext resourceContext) + { + var attributes = new Dictionary(); + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.EndObject: + { + return attributes; + } + case JsonTokenType.PropertyName: + { + string attributeName = reader.GetString(); + reader.Read(); + + AttrAttribute attribute = resourceContext.TryGetAttributeByPublicName(attributeName); + PropertyInfo property = attribute?.Property; + + if (property != null) + { + object attributeValue; + + if (property.Name == nameof(Identifiable.Id)) + { + attributeValue = JsonInvalidAttributeInfo.Id; + } + else + { + try + { + attributeValue = JsonSerializer.Deserialize(ref reader, property.PropertyType, options); + } + catch (JsonException) + { + // Inside a JsonConverter there is no way to know where in the JSON object tree we are. And the serializer + // is unable to provide the correct position either. So we avoid an exception and postpone producing an error + // response to the post-processing phase, by setting a sentinel value. + var jsonElement = ReadSubTree(ref reader, options); + + attributeValue = new JsonInvalidAttributeInfo(attributeName, property.PropertyType, jsonElement.ToString(), + jsonElement.ValueKind); + } + } + + attributes.Add(attributeName!, attributeValue); + } + else + { + reader.Skip(); + } + + break; + } + } + } + + throw GetEndOfStreamError(); + } + + /// + /// Ensures that attribute values are not wrapped in s. + /// + public override void Write(Utf8JsonWriter writer, ResourceObject value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + writer.WriteString(TypeText, value.Type); + + if (value.Id != null) + { + writer.WriteString(IdText, value.Id); + } + + if (value.Lid != null) + { + writer.WriteString(LidText, value.Lid); + } + + if (!value.Attributes.IsNullOrEmpty()) + { + writer.WritePropertyName(AttributesText); + WriteSubTree(writer, value.Attributes, options); + } + + if (!value.Relationships.IsNullOrEmpty()) + { + writer.WritePropertyName(RelationshipsText); + WriteSubTree(writer, value.Relationships, options); + } + + if (value.Links != null && value.Links.HasValue()) + { + writer.WritePropertyName(LinksText); + WriteSubTree(writer, value.Links, options); + } + + if (!value.Meta.IsNullOrEmpty()) + { + writer.WritePropertyName(MetaText); + WriteSubTree(writer, value.Meta, options); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs new file mode 100644 index 0000000000..0ca65c237e --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.JsonConverters +{ + /// + /// Converts to/from JSON. + /// + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class SingleOrManyDataConverterFactory : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(SingleOrManyData<>); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type objectType = typeToConvert.GetGenericArguments()[0]; + Type converterType = typeof(SingleOrManyDataConverter<>).MakeGenericType(objectType); + + return (JsonConverter)Activator.CreateInstance(converterType, BindingFlags.Instance | BindingFlags.Public, null, null, null); + } + + private sealed class SingleOrManyDataConverter : JsonObjectConverter> + where T : class, IResourceIdentity + { + public override SingleOrManyData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions serializerOptions) + { + var objects = new List(); + bool isManyData = false; + bool hasCompletedToMany = false; + + do + { + switch (reader.TokenType) + { + case JsonTokenType.EndArray: + { + hasCompletedToMany = true; + break; + } + case JsonTokenType.StartObject: + { + var resourceObject = ReadSubTree(ref reader, serializerOptions); + objects.Add(resourceObject); + break; + } + case JsonTokenType.StartArray: + { + isManyData = true; + break; + } + } + } + while (isManyData && !hasCompletedToMany && reader.Read()); + + object data = isManyData ? objects : objects.FirstOrDefault(); + return new SingleOrManyData(data); + } + + public override void Write(Utf8JsonWriter writer, SingleOrManyData value, JsonSerializerOptions options) + { + WriteSubTree(writer, value.Value, options); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyDocumentConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyDocumentConverter.cs new file mode 100644 index 0000000000..65d8f36d12 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyDocumentConverter.cs @@ -0,0 +1,86 @@ +using System; +using System.Text.Json; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.JsonConverters +{ + /// + /// Converts to JSON. + /// + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class WriteOnlyDocumentConverter : JsonObjectConverter + { + private static readonly JsonEncodedText JsonApiText = JsonEncodedText.Encode("jsonapi"); + private static readonly JsonEncodedText LinksText = JsonEncodedText.Encode("links"); + private static readonly JsonEncodedText DataText = JsonEncodedText.Encode("data"); + private static readonly JsonEncodedText AtomicOperationsText = JsonEncodedText.Encode("atomic:operations"); + private static readonly JsonEncodedText AtomicResultsText = JsonEncodedText.Encode("atomic:results"); + private static readonly JsonEncodedText ErrorsText = JsonEncodedText.Encode("errors"); + private static readonly JsonEncodedText IncludedText = JsonEncodedText.Encode("included"); + private static readonly JsonEncodedText MetaText = JsonEncodedText.Encode("meta"); + + public override Document Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotSupportedException("This converter cannot be used for reading JSON."); + } + + /// + /// Conditionally writes "data": null or omits it, depending on . + /// + public override void Write(Utf8JsonWriter writer, Document value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + if (value.JsonApi != null) + { + writer.WritePropertyName(JsonApiText); + WriteSubTree(writer, value.JsonApi, options); + } + + if (value.Links != null && value.Links.HasValue()) + { + writer.WritePropertyName(LinksText); + WriteSubTree(writer, value.Links, options); + } + + if (value.Data.IsAssigned) + { + writer.WritePropertyName(DataText); + WriteSubTree(writer, value.Data, options); + } + + if (!value.Operations.IsNullOrEmpty()) + { + writer.WritePropertyName(AtomicOperationsText); + WriteSubTree(writer, value.Operations, options); + } + + if (!value.Results.IsNullOrEmpty()) + { + writer.WritePropertyName(AtomicResultsText); + WriteSubTree(writer, value.Results, options); + } + + if (!value.Errors.IsNullOrEmpty()) + { + writer.WritePropertyName(ErrorsText); + WriteSubTree(writer, value.Errors, options); + } + + if (value.Included != null) + { + writer.WritePropertyName(IncludedText); + WriteSubTree(writer, value.Included, options); + } + + if (!value.Meta.IsNullOrEmpty()) + { + writer.WritePropertyName(MetaText); + WriteSubTree(writer, value.Meta, options); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyRelationshipObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyRelationshipObjectConverter.cs new file mode 100644 index 0000000000..d80fcee5bd --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyRelationshipObjectConverter.cs @@ -0,0 +1,51 @@ +using System; +using System.Text.Json; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.JsonConverters +{ + /// + /// Converts to JSON. + /// + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class WriteOnlyRelationshipObjectConverter : JsonObjectConverter + { + private static readonly JsonEncodedText DataText = JsonEncodedText.Encode("data"); + private static readonly JsonEncodedText LinksText = JsonEncodedText.Encode("links"); + private static readonly JsonEncodedText MetaText = JsonEncodedText.Encode("meta"); + + public override RelationshipObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotSupportedException("This converter cannot be used for reading JSON."); + } + + /// + /// Conditionally writes "data": null or omits it, depending on . + /// + public override void Write(Utf8JsonWriter writer, RelationshipObject value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + if (value.Links != null && value.Links.HasValue()) + { + writer.WritePropertyName(LinksText); + WriteSubTree(writer, value.Links, options); + } + + if (value.Data.IsAssigned) + { + writer.WritePropertyName(DataText); + WriteSubTree(writer, value.Data, options); + } + + if (!value.Meta.IsNullOrEmpty()) + { + writer.WritePropertyName(MetaText); + WriteSubTree(writer, value.Meta, options); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonInvalidAttributeInfo.cs b/src/JsonApiDotNetCore/Serialization/JsonInvalidAttributeInfo.cs new file mode 100644 index 0000000000..037eaf18af --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonInvalidAttributeInfo.cs @@ -0,0 +1,29 @@ +using System; +using System.Text.Json; + +namespace JsonApiDotNetCore.Serialization +{ + /// + /// A sentinel value that is temporarily stored in the attributes dictionary to postpone producing an error. + /// + internal sealed class JsonInvalidAttributeInfo + { + public static readonly JsonInvalidAttributeInfo Id = new("id", typeof(string), "-", JsonValueKind.Undefined); + + public string AttributeName { get; } + public Type AttributeType { get; } + public string JsonValue { get; } + public JsonValueKind JsonType { get; } + + public JsonInvalidAttributeInfo(string attributeName, Type attributeType, string jsonValue, JsonValueKind jsonType) + { + ArgumentGuard.NotNullNorEmpty(attributeName, nameof(attributeName)); + ArgumentGuard.NotNull(attributeType, nameof(attributeType)); + + AttributeName = attributeName; + AttributeType = attributeType; + JsonValue = jsonValue; + JsonType = jsonType; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonObjectConverter.cs new file mode 100644 index 0000000000..2a365317c4 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonObjectConverter.cs @@ -0,0 +1,35 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JsonApiDotNetCore.Serialization +{ + public abstract class JsonObjectConverter : JsonConverter + { + protected static TValue ReadSubTree(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + if (typeof(TValue) != typeof(object) && options?.GetConverter(typeof(TValue)) is JsonConverter converter) + { + return converter.Read(ref reader, typeof(TValue), options); + } + + return JsonSerializer.Deserialize(ref reader, options); + } + + protected static void WriteSubTree(Utf8JsonWriter writer, TValue value, JsonSerializerOptions options) + { + if (typeof(TValue) != typeof(object) && options?.GetConverter(typeof(TValue)) is JsonConverter converter) + { + converter.Write(writer, value, options); + } + else + { + JsonSerializer.Serialize(writer, value, options); + } + } + + protected static JsonException GetEndOfStreamError() + { + return new("Unexpected end of JSON stream."); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonSerializerExtensions.cs b/src/JsonApiDotNetCore/Serialization/JsonSerializerExtensions.cs deleted file mode 100644 index e08b9c3ce0..0000000000 --- a/src/JsonApiDotNetCore/Serialization/JsonSerializerExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace JsonApiDotNetCore.Serialization -{ - internal static class JsonSerializerExtensions - { - public static void ApplyErrorSettings(this JsonSerializer jsonSerializer) - { - jsonSerializer.NullValueHandling = NullValueHandling.Ignore; - - // JsonSerializer.Create() only performs a shallow copy of the shared settings, so we cannot change properties on its ContractResolver. - // But to serialize ErrorMeta.Data correctly, we need to ensure that JsonSerializer.ContractResolver.NamingStrategy.ProcessExtensionDataNames - // is set to 'true' while serializing errors. - var sharedContractResolver = (DefaultContractResolver)jsonSerializer.ContractResolver; - - jsonSerializer.ContractResolver = new DefaultContractResolver - { - NamingStrategy = new AlwaysProcessExtensionDataNamingStrategyWrapper(sharedContractResolver.NamingStrategy) - }; - } - - private sealed class AlwaysProcessExtensionDataNamingStrategyWrapper : NamingStrategy - { - private readonly NamingStrategy _namingStrategy; - - public AlwaysProcessExtensionDataNamingStrategyWrapper(NamingStrategy namingStrategy) - { - _namingStrategy = namingStrategy ?? new DefaultNamingStrategy(); - } - - public override string GetExtensionDataName(string name) - { - // Ignore the value of ProcessExtensionDataNames property on the wrapped strategy (short-circuit). - return ResolvePropertyName(name); - } - - public override string GetDictionaryKey(string key) - { - // Ignore the value of ProcessDictionaryKeys property on the wrapped strategy (short-circuit). - return ResolvePropertyName(key); - } - - public override string GetPropertyName(string name, bool hasSpecifiedName) - { - return _namingStrategy.GetPropertyName(name, hasSpecifiedName); - } - - protected override string ResolvePropertyName(string name) - { - return _namingStrategy.GetPropertyName(name, false); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationCode.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationCode.cs index f7852773b3..c016299024 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationCode.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationCode.cs @@ -1,12 +1,11 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json.Serialization; namespace JsonApiDotNetCore.Serialization.Objects { /// - /// See https://jsonapi.org/ext/atomic/#operation-objects. + /// See "op" in https://jsonapi.org/ext/atomic/#operation-objects. /// - [JsonConverter(typeof(StringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum AtomicOperationCode { Add, diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs index b6b4a134b8..27c3f58c44 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs @@ -1,25 +1,33 @@ using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { /// /// See https://jsonapi.org/ext/atomic/#operation-objects. /// - public sealed class AtomicOperationObject : ExposableData + [PublicAPI] + public sealed class AtomicOperationObject { - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] - public Dictionary Meta { get; set; } + [JsonPropertyName("data")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public SingleOrManyData Data { get; set; } - [JsonProperty("op")] - [JsonConverter(typeof(StringEnumConverter))] + [JsonPropertyName("op")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public AtomicOperationCode Code { get; set; } - [JsonProperty("ref", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("ref")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AtomicReference Ref { get; set; } - [JsonProperty("href", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("href")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Href { get; set; } + + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Meta { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs deleted file mode 100644 index b7352ed4c6..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Serialization.Objects -{ - /// - /// See https://jsonapi.org/ext/atomic/#document-structure. - /// - public sealed class AtomicOperationsDocument - { - /// - /// See "meta" in https://jsonapi.org/format/#document-top-level. - /// - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] - public IDictionary Meta { get; set; } - - /// - /// See "jsonapi" in https://jsonapi.org/format/#document-top-level. - /// - [JsonProperty("jsonapi", NullValueHandling = NullValueHandling.Ignore)] - public JsonApiObject JsonApi { get; set; } - - /// - /// See "links" in https://jsonapi.org/format/#document-top-level. - /// - [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] - public TopLevelLinks Links { get; set; } - - /// - /// See https://jsonapi.org/ext/atomic/#operation-objects. - /// - [JsonProperty("atomic:operations", NullValueHandling = NullValueHandling.Ignore)] - public IList Operations { get; set; } - - /// - /// See https://jsonapi.org/ext/atomic/#result-objects. - /// - [JsonProperty("atomic:results", NullValueHandling = NullValueHandling.Ignore)] - public IList Results { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs index 5847d33fe9..bff24ad299 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs @@ -1,20 +1,28 @@ -using System.Text; -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { /// - /// See 'ref' in https://jsonapi.org/ext/atomic/#operation-objects. + /// See "ref" in https://jsonapi.org/ext/atomic/#operation-objects. /// - public sealed class AtomicReference : ResourceIdentifierObject + [PublicAPI] + public sealed class AtomicReference : IResourceIdentity { - [JsonProperty("relationship", NullValueHandling = NullValueHandling.Ignore)] - public string Relationship { get; set; } + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string Type { get; set; } + + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Id { get; set; } - protected override void WriteMembers(StringBuilder builder) - { - base.WriteMembers(builder); - WriteMember(builder, "relationship", Relationship); - } + [JsonPropertyName("lid")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Lid { get; set; } + + [JsonPropertyName("relationship")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Relationship { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicResultObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicResultObject.cs index 44b5f691d7..14f67a5247 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicResultObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/AtomicResultObject.cs @@ -1,14 +1,21 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { /// /// See https://jsonapi.org/ext/atomic/#result-objects. /// - public sealed class AtomicResultObject : ExposableData + [PublicAPI] + public sealed class AtomicResultObject { - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] - public Dictionary Meta { get; set; } + [JsonPropertyName("data")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public SingleOrManyData Data { get; set; } + + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Meta { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs index 56c79f2b86..ae3a09b9b1 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs @@ -1,35 +1,64 @@ +using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Linq; +using System.Net; +using System.Text.Json.Serialization; namespace JsonApiDotNetCore.Serialization.Objects { /// - /// https://jsonapi.org/format/#document-structure + /// See https://jsonapi.org/format/1.1/#document-top-level and https://jsonapi.org/ext/atomic/#document-structure. /// - public sealed class Document : ExposableData + public sealed class Document { - /// - /// see "meta" in https://jsonapi.org/format/#document-top-level - /// - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] - public IDictionary Meta { get; set; } - - /// - /// see "jsonapi" in https://jsonapi.org/format/#document-top-level - /// - [JsonProperty("jsonapi", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("jsonapi")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public JsonApiObject JsonApi { get; set; } - /// - /// see "links" in https://jsonapi.org/format/#document-top-level - /// - [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("links")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public TopLevelLinks Links { get; set; } - /// - /// see "included" in https://jsonapi.org/format/#document-top-level - /// - [JsonProperty("included", NullValueHandling = NullValueHandling.Ignore, Order = 1)] + [JsonPropertyName("data")] + // JsonIgnoreCondition is determined at runtime by WriteOnlyDocumentConverter. + public SingleOrManyData Data { get; set; } + + [JsonPropertyName("atomic:operations")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IList Operations { get; set; } + + [JsonPropertyName("atomic:results")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IList Results { get; set; } + + [JsonPropertyName("errors")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IList Errors { get; set; } + + [JsonPropertyName("included")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IList Included { get; set; } + + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Meta { get; set; } + + internal HttpStatusCode GetErrorStatusCode() + { + if (Errors.IsNullOrEmpty()) + { + throw new InvalidOperationException("No errors found."); + } + + int[] statusCodes = Errors.Select(error => (int)error.StatusCode).Distinct().ToArray(); + + if (statusCodes.Length == 1) + { + return (HttpStatusCode)statusCodes[0]; + } + + int statusCode = int.Parse($"{statusCodes.Max().ToString()[0]}00"); + return (HttpStatusCode)statusCode; + } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/Error.cs b/src/JsonApiDotNetCore/Serialization/Objects/Error.cs deleted file mode 100644 index f3afb594e8..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/Error.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using JetBrains.Annotations; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Serialization.Objects -{ - /// - /// Provides additional information about a problem encountered while performing an operation. Error objects MUST be returned as an array keyed by errors - /// in the top level of a JSON:API document. - /// - [PublicAPI] - public sealed class Error - { - /// - /// A unique identifier for this particular occurrence of the problem. - /// - [JsonProperty] - public string Id { get; set; } = Guid.NewGuid().ToString(); - - /// - /// A link that leads to further details about this particular occurrence of the problem. - /// - [JsonProperty] - public ErrorLinks Links { get; set; } = new(); - - /// - /// The HTTP status code applicable to this problem. - /// - [JsonIgnore] - public HttpStatusCode StatusCode { get; set; } - - [JsonProperty] - public string Status - { - get => StatusCode.ToString("d"); - set => StatusCode = (HttpStatusCode)int.Parse(value); - } - - /// - /// An application-specific error code. - /// - [JsonProperty] - public string Code { get; set; } - - /// - /// A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of - /// localization. - /// - [JsonProperty] - public string Title { get; set; } - - /// - /// A human-readable explanation specific to this occurrence of the problem. Like title, this field's value can be localized. - /// - [JsonProperty] - public string Detail { get; set; } - - /// - /// An object containing references to the source of the error. - /// - [JsonProperty] - public ErrorSource Source { get; set; } = new(); - - /// - /// An object containing non-standard meta-information (key/value pairs) about the error. - /// - [JsonProperty] - public ErrorMeta Meta { get; set; } = new(); - - public Error(HttpStatusCode statusCode) - { - StatusCode = statusCode; - } - - public bool ShouldSerializeLinks() - { - return Links?.About != null; - } - - public bool ShouldSerializeSource() - { - return Source != null && (Source.Pointer != null || Source.Parameter != null); - } - - public bool ShouldSerializeMeta() - { - return Meta != null && Meta.Data.Any(); - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs deleted file mode 100644 index 971fdecce3..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using JetBrains.Annotations; - -namespace JsonApiDotNetCore.Serialization.Objects -{ - [PublicAPI] - public sealed class ErrorDocument - { - public IReadOnlyList Errors { get; } - - public ErrorDocument() - : this(Array.Empty()) - { - } - - public ErrorDocument(Error error) - : this(error.AsEnumerable()) - { - } - - public ErrorDocument(IEnumerable errors) - { - ArgumentGuard.NotNull(errors, nameof(errors)); - - Errors = errors.ToList(); - } - - public HttpStatusCode GetErrorStatusCode() - { - int[] statusCodes = Errors.Select(error => (int)error.StatusCode).Distinct().ToArray(); - - if (statusCodes.Length == 1) - { - return (HttpStatusCode)statusCodes[0]; - } - - int statusCode = int.Parse(statusCodes.Max().ToString()[0] + "00"); - return (HttpStatusCode)statusCode; - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorLinks.cs index 16be6392e1..3b03f68cda 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorLinks.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorLinks.cs @@ -1,13 +1,20 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { + /// + /// See "links" in https://jsonapi.org/format/1.1/#error-objects. + /// + [PublicAPI] public sealed class ErrorLinks { - /// - /// A URL that leads to further details about this particular occurrence of the problem. - /// - [JsonProperty] + [JsonPropertyName("about")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string About { get; set; } + + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Type { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs deleted file mode 100644 index 1589089719..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Serialization.Objects -{ - /// - /// A meta object containing non-standard meta-information about the error. - /// - [PublicAPI] - public sealed class ErrorMeta - { - [JsonExtensionData] - public IDictionary Data { get; } = new Dictionary(); - - public void IncludeExceptionStackTrace(Exception exception) - { - if (exception == null) - { - Data.Remove("StackTrace"); - } - else - { - Data["StackTrace"] = exception.ToString().Split("\n", int.MaxValue, StringSplitOptions.RemoveEmptyEntries); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorObject.cs new file mode 100644 index 0000000000..a5ac6be1a8 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorObject.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json.Serialization; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.Serialization.Objects +{ + /// + /// See https://jsonapi.org/format/1.1/#error-objects. + /// + [PublicAPI] + public sealed class ErrorObject + { + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [JsonPropertyName("links")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ErrorLinks Links { get; set; } + + [JsonIgnore] + public HttpStatusCode StatusCode { get; set; } + + [JsonPropertyName("status")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string Status + { + get => StatusCode.ToString("d"); + set => StatusCode = (HttpStatusCode)int.Parse(value); + } + + [JsonPropertyName("code")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Code { get; set; } + + [JsonPropertyName("title")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Title { get; set; } + + [JsonPropertyName("detail")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Detail { get; set; } + + [JsonPropertyName("source")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ErrorSource Source { get; set; } + + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Meta { get; set; } + + public ErrorObject(HttpStatusCode statusCode) + { + StatusCode = statusCode; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs index 88cdc1812d..ebd8ee49bd 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs @@ -1,20 +1,24 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { + /// + /// See "source" in https://jsonapi.org/format/1.1/#error-objects. + /// + [PublicAPI] public sealed class ErrorSource { - /// - /// Optional. A JSON Pointer [RFC6901] to the associated resource in the request document [e.g. "/data" for a primary data object, or - /// "/data/attributes/title" for a specific attribute]. - /// - [JsonProperty] + [JsonPropertyName("pointer")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Pointer { get; set; } - /// - /// Optional. A string indicating which URI query parameter caused the error. - /// - [JsonProperty] + [JsonPropertyName("parameter")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Parameter { get; set; } + + [JsonPropertyName("header")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Header { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs b/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs deleted file mode 100644 index 27ef8d0690..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace JsonApiDotNetCore.Serialization.Objects -{ - [PublicAPI] - public abstract class ExposableData - where TResource : class - { - private bool IsEmpty => !HasManyData && SingleData == null; - - private bool HasManyData => IsManyData && ManyData.Any(); - - /// - /// Internally used to indicate if the document's primary data should still be serialized when it's value is null. This is used when a single resource is - /// requested but not present (eg /articles/1/author). - /// - internal bool IsPopulated { get; private set; } - - internal bool HasResource => IsPopulated && !IsEmpty; - - /// - /// See "primary data" in https://jsonapi.org/format/#document-top-level. - /// - [JsonProperty("data")] - public object Data - { - get => GetPrimaryData(); - set => SetPrimaryData(value); - } - - /// - /// Internally used for "single" primary data. - /// - [JsonIgnore] - public TResource SingleData { get; private set; } - - /// - /// Internally used for "many" primary data. - /// - [JsonIgnore] - public IList ManyData { get; private set; } - - /// - /// Indicates if the document's primary data is "single" or "many". - /// - [JsonIgnore] - public bool IsManyData { get; private set; } - - /// - /// See https://www.newtonsoft.com/json/help/html/ConditionalProperties.htm. - /// - /// - /// Moving this method to the derived class where it is needed only in the case of would make more sense, but Newtonsoft - /// does not support this. - /// - public bool ShouldSerializeData() - { - if (GetType() == typeof(RelationshipEntry)) - { - return IsPopulated; - } - - return true; - } - - /// - /// Gets the "single" or "many" data depending on which one was assigned in this document. - /// - protected object GetPrimaryData() - { - if (IsManyData) - { - return ManyData; - } - - return SingleData; - } - - /// - /// Sets the primary data depending on if it is "single" or "many" data. - /// - protected void SetPrimaryData(object value) - { - IsPopulated = true; - - if (value is JObject jObject) - { - SingleData = jObject.ToObject(); - } - else if (value is TResource ro) - { - SingleData = ro; - } - else if (value != null) - { - IsManyData = true; - - if (value is JArray jArray) - { - ManyData = jArray.ToObject>(); - } - else - { - ManyData = (List)value; - } - } - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs b/src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs new file mode 100644 index 0000000000..ff936f4d46 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs @@ -0,0 +1,9 @@ +namespace JsonApiDotNetCore.Serialization.Objects +{ + public interface IResourceIdentity + { + public string Type { get; } + public string Id { get; } + public string Lid { get; } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/JsonApiObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/JsonApiObject.cs index 66682fb6a2..11b214b434 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/JsonApiObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/JsonApiObject.cs @@ -1,26 +1,29 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { /// - /// https://jsonapi.org/format/1.1/#document-jsonapi-object. + /// See https://jsonapi.org/format/1.1/#document-jsonapi-object. /// + [PublicAPI] public sealed class JsonApiObject { - [JsonProperty("version", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Version { get; set; } - [JsonProperty("ext", NullValueHandling = NullValueHandling.Ignore)] - public ICollection Ext { get; set; } + [JsonPropertyName("ext")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IList Ext { get; set; } - [JsonProperty("profile", NullValueHandling = NullValueHandling.Ignore)] - public ICollection Profile { get; set; } + [JsonPropertyName("profile")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IList Profile { get; set; } - /// - /// see "meta" in https://jsonapi.org/format/1.1/#document-meta - /// - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary Meta { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/RelationshipEntry.cs b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipEntry.cs deleted file mode 100644 index 8ebbbf1b16..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/RelationshipEntry.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Serialization.Objects -{ - public sealed class RelationshipEntry : ExposableData - { - [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] - public RelationshipLinks Links { get; set; } - - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] - public IDictionary Meta { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/RelationshipLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipLinks.cs index e0ee4c680f..b66f33daa8 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/RelationshipLinks.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipLinks.cs @@ -1,19 +1,20 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { + /// + /// See "links" in https://jsonapi.org/format/1.1/#document-resource-object-relationships. + /// + [PublicAPI] public sealed class RelationshipLinks { - /// - /// See "links" bulletin at https://jsonapi.org/format/#document-resource-object-relationships. - /// - [JsonProperty("self", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("self")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Self { get; set; } - /// - /// See https://jsonapi.org/format/#document-resource-object-related-resource-links. - /// - [JsonProperty("related", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("related")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Related { get; set; } internal bool HasValue() diff --git a/src/JsonApiDotNetCore/Serialization/Objects/RelationshipObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipObject.cs new file mode 100644 index 0000000000..fb4296d70d --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipObject.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.Serialization.Objects +{ + /// + /// See https://jsonapi.org/format/1.1/#document-resource-object-relationships. + /// + [PublicAPI] + public sealed class RelationshipObject + { + [JsonPropertyName("links")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public RelationshipLinks Links { get; set; } + + [JsonPropertyName("data")] + // JsonIgnoreCondition is determined at runtime by WriteOnlyRelationshipObjectConverter. + public SingleOrManyData Data { get; set; } + + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Meta { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs index 672255d96e..de4104d28a 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs @@ -1,50 +1,29 @@ -using System.Text; -using Newtonsoft.Json; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { - public class ResourceIdentifierObject + /// + /// See https://jsonapi.org/format/1.1/#document-resource-identifier-objects. + /// + [PublicAPI] + public sealed class ResourceIdentifierObject : IResourceIdentity { - [JsonProperty("type", Order = -4)] + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public string Type { get; set; } - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore, Order = -3)] + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Id { get; set; } - [JsonProperty("lid", NullValueHandling = NullValueHandling.Ignore, Order = -2)] + [JsonPropertyName("lid")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Lid { get; set; } - public override string ToString() - { - var builder = new StringBuilder(); - - WriteMembers(builder); - builder.Insert(0, GetType().Name + ": "); - - return builder.ToString(); - } - - protected virtual void WriteMembers(StringBuilder builder) - { - WriteMember(builder, "type", Type); - WriteMember(builder, "id", Id); - WriteMember(builder, "lid", Lid); - } - - protected static void WriteMember(StringBuilder builder, string memberName, string memberValue) - { - if (memberValue != null) - { - if (builder.Length > 0) - { - builder.Append(", "); - } - - builder.Append(memberName); - builder.Append("=\""); - builder.Append(memberValue); - builder.Append('"'); - } - } + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Meta { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceLinks.cs index 3afbd3ffbd..7ab1f6861e 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ResourceLinks.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceLinks.cs @@ -1,13 +1,16 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { + /// + /// See https://jsonapi.org/format/1.1/#document-resource-object-links. + /// + [PublicAPI] public sealed class ResourceLinks { - /// - /// See https://jsonapi.org/format/#document-resource-object-links. - /// - [JsonProperty("self", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("self")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Self { get; set; } internal bool HasValue() diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs index c3ae877787..f418a63ed1 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs @@ -1,20 +1,41 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { - public sealed class ResourceObject : ResourceIdentifierObject + /// + /// See https://jsonapi.org/format/1.1/#document-resource-objects. + /// + [PublicAPI] + public sealed class ResourceObject : IResourceIdentity { - [JsonProperty("attributes", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string Type { get; set; } + + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Id { get; set; } + + [JsonPropertyName("lid")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Lid { get; set; } + + [JsonPropertyName("attributes")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary Attributes { get; set; } - [JsonProperty("relationships", NullValueHandling = NullValueHandling.Ignore)] - public IDictionary Relationships { get; set; } + [JsonPropertyName("relationships")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Relationships { get; set; } - [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("links")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ResourceLinks Links { get; set; } - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary Meta { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs b/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs new file mode 100644 index 0000000000..c2a6c23876 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.JsonConverters; + +namespace JsonApiDotNetCore.Serialization.Objects +{ + /// + /// Represents the value of the "data" element, which is either null, a single object or an array of objects. Add + /// to to properly roundtrip. + /// + [PublicAPI] + public readonly struct SingleOrManyData + where T : class, IResourceIdentity + { + // ReSharper disable once MergeConditionalExpression + // Justification: ReSharper reporting this is a bug, which is fixed in v2021.2.1. This condition cannot be merged. + public object Value => ManyValue != null ? ManyValue : SingleValue; + + [JsonIgnore] + public bool IsAssigned { get; } + + [JsonIgnore] + public T SingleValue { get; } + + [JsonIgnore] + public IList ManyValue { get; } + + public SingleOrManyData(object value) + { + IsAssigned = true; + + if (value is IEnumerable manyData) + { + ManyValue = manyData.ToList(); + SingleValue = null; + } + else + { + ManyValue = null; + SingleValue = (T)value; + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs index 6970eecb8a..0817e56d8a 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs @@ -1,73 +1,46 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { /// - /// See links section in https://jsonapi.org/format/#document-top-level. + /// See "links" in https://jsonapi.org/format/1.1/#document-top-level. /// + [PublicAPI] public sealed class TopLevelLinks { - [JsonProperty("self")] + [JsonPropertyName("self")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Self { get; set; } - [JsonProperty("related")] + [JsonPropertyName("related")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Related { get; set; } - [JsonProperty("describedby")] + [JsonPropertyName("describedby")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string DescribedBy { get; set; } - [JsonProperty("first")] + [JsonPropertyName("first")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string First { get; set; } - [JsonProperty("last")] + [JsonPropertyName("last")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Last { get; set; } - [JsonProperty("prev")] + [JsonPropertyName("prev")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Prev { get; set; } - [JsonProperty("next")] + [JsonPropertyName("next")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Next { get; set; } - // http://www.newtonsoft.com/json/help/html/ConditionalProperties.htm - public bool ShouldSerializeSelf() - { - return !string.IsNullOrEmpty(Self); - } - - public bool ShouldSerializeRelated() - { - return !string.IsNullOrEmpty(Related); - } - - public bool ShouldSerializeDescribedBy() - { - return !string.IsNullOrEmpty(DescribedBy); - } - - public bool ShouldSerializeFirst() - { - return !string.IsNullOrEmpty(First); - } - - public bool ShouldSerializeLast() - { - return !string.IsNullOrEmpty(Last); - } - - public bool ShouldSerializePrev() - { - return !string.IsNullOrEmpty(Prev); - } - - public bool ShouldSerializeNext() - { - return !string.IsNullOrEmpty(Next); - } - internal bool HasValue() { - return ShouldSerializeSelf() || ShouldSerializeRelated() || ShouldSerializeDescribedBy() || ShouldSerializeFirst() || ShouldSerializeLast() || - ShouldSerializePrev() || ShouldSerializeNext(); + return !string.IsNullOrEmpty(Self) || !string.IsNullOrEmpty(Related) || !string.IsNullOrEmpty(DescribedBy) || !string.IsNullOrEmpty(First) || + !string.IsNullOrEmpty(Last) || !string.IsNullOrEmpty(Prev) || !string.IsNullOrEmpty(Next); } } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs index 8220936b20..3b466a7087 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs @@ -6,14 +6,12 @@ using Humanizer; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Resources.Internal; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; -using Newtonsoft.Json.Linq; namespace JsonApiDotNetCore.Serialization { @@ -29,9 +27,9 @@ public class RequestDeserializer : BaseDeserializer, IJsonApiDeserializer private readonly IJsonApiOptions _options; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; - public RequestDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields, + public RequestDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory, ITargetedFields targetedFields, IHttpContextAccessor httpContextAccessor, IJsonApiRequest request, IJsonApiOptions options, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(resourceContextProvider, resourceFactory) + : base(resourceGraph, resourceFactory) { ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); @@ -61,7 +59,7 @@ public object Deserialize(string body) return DeserializeOperationsDocument(body); } - object instance = DeserializeBody(body); + object instance = DeserializeData(body, _options.SerializerReadOptions); if (instance is IIdentifiable resource && _request.Kind != EndpointKind.Relationship) { @@ -75,13 +73,7 @@ public object Deserialize(string body) private object DeserializeOperationsDocument(string body) { - AtomicOperationsDocument document; - - using (CodeTimingSessionManager.Current.Measure("Newtonsoft.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages)) - { - JToken bodyToken = LoadJToken(body); - document = bodyToken.ToObject(); - } + Document document = DeserializeDocument(body, _options.SerializerReadOptions); if ((document?.Operations).IsNullOrEmpty()) { @@ -216,7 +208,7 @@ private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperati _request.CopyFrom(request); - IIdentifiable primaryResource = ParseResourceObject(operation.SingleData); + IIdentifiable primaryResource = ParseResourceObject(operation.Data.SingleValue); _resourceDefinitionAccessor.OnDeserialize(primaryResource); @@ -236,33 +228,33 @@ private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperati private ResourceObject GetRequiredSingleDataForResourceOperation(AtomicOperationObject operation) { - if (operation.Data == null) + if (operation.Data.Value == null) { throw new JsonApiSerializationException("The 'data' element is required.", null, atomicOperationIndex: AtomicOperationIndex); } - if (operation.SingleData == null) + if (operation.Data.SingleValue == null) { throw new JsonApiSerializationException("Expected single data element for create/update resource operation.", null, atomicOperationIndex: AtomicOperationIndex); } - return operation.SingleData; + return operation.Data.SingleValue; } [AssertionMethod] - private void AssertElementHasType(ResourceIdentifierObject resourceIdentifierObject, string elementPath) + private void AssertElementHasType(IResourceIdentity resourceIdentity, string elementPath) { - if (resourceIdentifierObject.Type == null) + if (resourceIdentity.Type == null) { throw new JsonApiSerializationException($"The '{elementPath}.type' element is required.", null, atomicOperationIndex: AtomicOperationIndex); } } - private void AssertElementHasIdOrLid(ResourceIdentifierObject resourceIdentifierObject, string elementPath, bool isRequired) + private void AssertElementHasIdOrLid(IResourceIdentity resourceIdentity, string elementPath, bool isRequired) { - bool hasNone = resourceIdentifierObject.Id == null && resourceIdentifierObject.Lid == null; - bool hasBoth = resourceIdentifierObject.Id != null && resourceIdentifierObject.Lid != null; + bool hasNone = resourceIdentity.Id == null && resourceIdentity.Lid == null; + bool hasBoth = resourceIdentity.Id != null && resourceIdentity.Lid != null; if (isRequired ? hasNone || hasBoth : hasBoth) { @@ -271,13 +263,13 @@ private void AssertElementHasIdOrLid(ResourceIdentifierObject resourceIdentifier } } - private void AssertCompatibleId(ResourceIdentifierObject resourceIdentifierObject, Type idType) + private void AssertCompatibleId(IResourceIdentity resourceIdentity, Type idType) { - if (resourceIdentifierObject.Id != null) + if (resourceIdentity.Id != null) { try { - RuntimeTypeConverter.ConvertType(resourceIdentifierObject.Id, idType); + RuntimeTypeConverter.ConvertType(resourceIdentity.Id, idType); } catch (FormatException exception) { @@ -286,33 +278,33 @@ private void AssertCompatibleId(ResourceIdentifierObject resourceIdentifierObjec } } - private void AssertSameIdentityInRefData(AtomicOperationObject operation, ResourceIdentifierObject resourceIdentifierObject) + private void AssertSameIdentityInRefData(AtomicOperationObject operation, IResourceIdentity resourceIdentity) { - if (operation.Ref.Id != null && resourceIdentifierObject.Id != null && resourceIdentifierObject.Id != operation.Ref.Id) + if (operation.Ref.Id != null && resourceIdentity.Id != null && resourceIdentity.Id != operation.Ref.Id) { throw new JsonApiSerializationException("Resource ID mismatch between 'ref.id' and 'data.id' element.", - $"Expected resource with ID '{operation.Ref.Id}' in 'data.id', instead of '{resourceIdentifierObject.Id}'.", + $"Expected resource with ID '{operation.Ref.Id}' in 'data.id', instead of '{resourceIdentity.Id}'.", atomicOperationIndex: AtomicOperationIndex); } - if (operation.Ref.Lid != null && resourceIdentifierObject.Lid != null && resourceIdentifierObject.Lid != operation.Ref.Lid) + if (operation.Ref.Lid != null && resourceIdentity.Lid != null && resourceIdentity.Lid != operation.Ref.Lid) { throw new JsonApiSerializationException("Resource local ID mismatch between 'ref.lid' and 'data.lid' element.", - $"Expected resource with local ID '{operation.Ref.Lid}' in 'data.lid', instead of '{resourceIdentifierObject.Lid}'.", + $"Expected resource with local ID '{operation.Ref.Lid}' in 'data.lid', instead of '{resourceIdentity.Lid}'.", atomicOperationIndex: AtomicOperationIndex); } - if (operation.Ref.Id != null && resourceIdentifierObject.Lid != null) + if (operation.Ref.Id != null && resourceIdentity.Lid != null) { throw new JsonApiSerializationException("Resource identity mismatch between 'ref.id' and 'data.lid' element.", - $"Expected resource with ID '{operation.Ref.Id}' in 'data.id', instead of '{resourceIdentifierObject.Lid}' in 'data.lid'.", + $"Expected resource with ID '{operation.Ref.Id}' in 'data.id', instead of '{resourceIdentity.Lid}' in 'data.lid'.", atomicOperationIndex: AtomicOperationIndex); } - if (operation.Ref.Lid != null && resourceIdentifierObject.Id != null) + if (operation.Ref.Lid != null && resourceIdentity.Id != null) { throw new JsonApiSerializationException("Resource identity mismatch between 'ref.lid' and 'data.id' element.", - $"Expected resource with local ID '{operation.Ref.Lid}' in 'data.lid', instead of '{resourceIdentifierObject.Id}' in 'data.id'.", + $"Expected resource with local ID '{operation.Ref.Lid}' in 'data.lid', instead of '{resourceIdentity.Id}' in 'data.id'.", atomicOperationIndex: AtomicOperationIndex); } } @@ -362,7 +354,7 @@ private OperationContainer ParseForRelationshipOperation(AtomicOperationObject o $"Relationship '{operation.Ref.Relationship}' must be a to-many relationship.", atomicOperationIndex: AtomicOperationIndex); } - ResourceContext secondaryResourceContext = ResourceContextProvider.GetResourceContext(relationship.RightType); + ResourceContext secondaryResourceContext = ResourceGraph.GetResourceContext(relationship.RightType); var request = new JsonApiRequest { @@ -392,7 +384,7 @@ private OperationContainer ParseForRelationshipOperation(AtomicOperationObject o private RelationshipAttribute GetExistingRelationship(AtomicReference reference, ResourceContext resourceContext) { - RelationshipAttribute relationship = resourceContext.Relationships.FirstOrDefault(attribute => attribute.PublicName == reference.Relationship); + RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(reference.Relationship); if (relationship == null) { @@ -409,23 +401,23 @@ private void ParseDataForRelationship(RelationshipAttribute relationship, Resour { if (relationship is HasOneAttribute) { - if (operation.ManyData != null) + if (operation.Data.ManyValue != null) { throw new JsonApiSerializationException("Expected single data element for to-one relationship.", $"Expected single data element for '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } - if (operation.SingleData != null) + if (operation.Data.SingleValue != null) { - ValidateSingleDataForRelationship(operation.SingleData, secondaryResourceContext, "data"); + ValidateSingleDataForRelationship(operation.Data.SingleValue, secondaryResourceContext, "data"); - IIdentifiable secondaryResource = ParseResourceObject(operation.SingleData); + IIdentifiable secondaryResource = ParseResourceObject(operation.Data.SingleValue); relationship.SetValue(primaryResource, secondaryResource); } } else if (relationship is HasManyAttribute) { - if (operation.ManyData == null) + if (operation.Data.ManyValue == null) { throw new JsonApiSerializationException("Expected data[] element for to-many relationship.", $"Expected data[] element for '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); @@ -433,7 +425,7 @@ private void ParseDataForRelationship(RelationshipAttribute relationship, Resour var secondaryResources = new List(); - foreach (ResourceObject resourceObject in operation.ManyData) + foreach (ResourceObject resourceObject in operation.Data.ManyValue) { ValidateSingleDataForRelationship(resourceObject, secondaryResourceContext, "data[]"); @@ -488,7 +480,7 @@ private void AssertResourceIdIsNotTargeted(ITargetedFields targetedFields) /// /// Relationship data for . Is null when is not a . /// - protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) + protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipObject data = null) { bool isCreatingResource = IsCreatingResource(); bool isUpdatingResource = IsUpdatingResource(); diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs index bc5fd805c0..348e1d7a2d 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs @@ -8,7 +8,6 @@ using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization { @@ -72,7 +71,7 @@ public string Serialize(object content) return SerializeMany(collectionOfIdentifiable.ToArray()); } - if (content is ErrorDocument errorDocument) + if (content is Document errorDocument) { return SerializeErrorDocument(errorDocument); } @@ -80,12 +79,11 @@ public string Serialize(object content) throw new InvalidOperationException("Data being returned must be errors or resources."); } - private string SerializeErrorDocument(ErrorDocument errorDocument) + private string SerializeErrorDocument(Document document) { - return SerializeObject(errorDocument, _options.SerializerSettings, serializer => - { - serializer.ApplyErrorSettings(); - }); + SetApiVersion(document); + + return SerializeObject(document, _options.SerializerWriteOptions); } /// @@ -105,7 +103,7 @@ internal string SerializeSingle(IIdentifiable resource) IReadOnlyCollection relationships = _fieldsToSerialize.GetRelationships(_primaryResourceType); Document document = Build(resource, attributes, relationships); - ResourceObject resourceObject = document.SingleData; + ResourceObject resourceObject = document.Data.SingleValue; if (resourceObject != null) { @@ -114,10 +112,7 @@ internal string SerializeSingle(IIdentifiable resource) AddTopLevelObjects(document); - return SerializeObject(document, _options.SerializerSettings, serializer => - { - serializer.NullValueHandling = NullValueHandling.Include; - }); + return SerializeObject(document, _options.SerializerWriteOptions); } /// @@ -141,7 +136,7 @@ internal string SerializeMany(IReadOnlyCollection resources) Document document = Build(resources, attributes, relationships); - foreach (ResourceObject resourceObject in document.ManyData) + foreach (ResourceObject resourceObject in document.Data.ManyValue) { ResourceLinks links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); @@ -155,16 +150,22 @@ internal string SerializeMany(IReadOnlyCollection resources) AddTopLevelObjects(document); - return SerializeObject(document, _options.SerializerSettings, serializer => - { - serializer.NullValueHandling = NullValueHandling.Include; - }); + return SerializeObject(document, _options.SerializerWriteOptions); } /// /// Adds top-level objects that are only added to a document in the case of server-side serialization. /// private void AddTopLevelObjects(Document document) + { + SetApiVersion(document); + + document.Links = _linkBuilder.GetTopLevelLinks(); + document.Meta = _metaBuilder.Build(); + document.Included = _includedBuilder.Build(); + } + + private void SetApiVersion(Document document) { if (_options.IncludeJsonApiVersion) { @@ -173,10 +174,6 @@ private void AddTopLevelObjects(Document document) Version = "1.1" }; } - - document.Links = _linkBuilder.GetTopLevelLinks(); - document.Meta = _metaBuilder.Build(); - document.Included = _includedBuilder.Build(); } } } diff --git a/src/JsonApiDotNetCore/TypeExtensions.cs b/src/JsonApiDotNetCore/TypeExtensions.cs index d25599b821..15713ac5fa 100644 --- a/src/JsonApiDotNetCore/TypeExtensions.cs +++ b/src/JsonApiDotNetCore/TypeExtensions.cs @@ -19,5 +19,30 @@ public static bool IsOrImplementsInterface(this Type source, Type interfaceType) return source == interfaceType || source.GetInterfaces().Any(type => type == interfaceType); } + + /// + /// Gets the name of a type, including the names of its generic type parameters. + /// + /// > + /// ]]> + /// + /// + public static string GetFriendlyTypeName(this Type type) + { + ArgumentGuard.NotNull(type, nameof(type)); + + // Based on https://stackoverflow.com/questions/2581642/how-do-i-get-the-type-name-of-a-generic-type-argument. + + if (type.IsGenericType) + { + string genericArguments = type.GetGenericArguments().Select(GetFriendlyTypeName) + .Aggregate((firstType, secondType) => $"{firstType}, {secondType}"); + + return $"{type.Name[..type.Name.IndexOf("`", StringComparison.Ordinal)]}" + $"<{genericArguments}>"; + } + + return type.Name; + } } } diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 8392b92baa..a5a2f9fd77 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -33,7 +33,6 @@ public ServiceDiscoveryFacadeTests() _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); - _services.AddScoped(_ => new Mock().Object); _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); @@ -57,10 +56,10 @@ public void Can_add_resources_from_assembly_to_graph() // Assert IResourceGraph resourceGraph = _resourceGraphBuilder.Build(); - ResourceContext personContext = resourceGraph.GetResourceContext(typeof(Person)); + ResourceContext personContext = resourceGraph.TryGetResourceContext(typeof(Person)); personContext.Should().NotBeNull(); - ResourceContext todoItemContext = resourceGraph.GetResourceContext(typeof(TodoItem)); + ResourceContext todoItemContext = resourceGraph.TryGetResourceContext(typeof(TodoItem)); todoItemContext.Should().NotBeNull(); } @@ -77,7 +76,7 @@ public void Can_add_resource_from_current_assembly_to_graph() // Assert IResourceGraph resourceGraph = _resourceGraphBuilder.Build(); - ResourceContext testContext = resourceGraph.GetResourceContext(typeof(TestResource)); + ResourceContext testContext = resourceGraph.TryGetResourceContext(typeof(TestResource)); testContext.Should().NotBeNull(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs index d1a77728ac..2634ffae2a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs @@ -44,7 +44,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/televisionBroadcasts/" + broadcast.StringId; + string route = $"/televisionBroadcasts/{broadcast.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -52,9 +52,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(broadcast.StringId); - responseDocument.SingleData.Attributes["archivedAt"].Should().BeCloseTo(broadcast.ArchivedAt); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(broadcast.StringId); + responseDocument.Data.SingleValue.Attributes["archivedAt"].As().Should().BeCloseTo(broadcast.ArchivedAt.GetValueOrDefault()); } [Fact] @@ -70,7 +70,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/televisionBroadcasts/" + broadcast.StringId; + string route = $"/televisionBroadcasts/{broadcast.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -78,9 +78,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(broadcast.StringId); - responseDocument.SingleData.Attributes["archivedAt"].Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(broadcast.StringId); + responseDocument.Data.SingleValue.Attributes["archivedAt"].As().Should().BeNull(); } [Fact] @@ -105,9 +105,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(broadcasts[1].StringId); - responseDocument.ManyData[0].Attributes["archivedAt"].Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(broadcasts[1].StringId); + responseDocument.Data.ManyValue[0].Attributes["archivedAt"].As().Should().BeNull(); } [Fact] @@ -132,11 +132,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(broadcasts[0].StringId); - responseDocument.ManyData[0].Attributes["archivedAt"].Should().BeCloseTo(broadcasts[0].ArchivedAt); - responseDocument.ManyData[1].Id.Should().Be(broadcasts[1].StringId); - responseDocument.ManyData[1].Attributes["archivedAt"].Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(broadcasts[0].StringId); + responseDocument.Data.ManyValue[0].Attributes["archivedAt"].As().Should().BeCloseTo(broadcasts[0].ArchivedAt.GetValueOrDefault()); + responseDocument.Data.ManyValue[1].Id.Should().Be(broadcasts[1].StringId); + responseDocument.Data.ManyValue[1].Attributes["archivedAt"].Should().BeNull(); } [Fact] @@ -161,8 +161,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(station.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(station.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); @@ -192,12 +192,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(station.StringId); + DateTimeOffset archivedAt0 = station.Broadcasts.ElementAt(0).ArchivedAt.GetValueOrDefault(); + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(station.StringId); responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(station.Broadcasts.ElementAt(0).StringId); - responseDocument.Included[0].Attributes["archivedAt"].Should().BeCloseTo(station.Broadcasts.ElementAt(0).ArchivedAt); + responseDocument.Included[0].Attributes["archivedAt"].As().Should().BeCloseTo(archivedAt0); responseDocument.Included[1].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); responseDocument.Included[1].Attributes["archivedAt"].Should().BeNull(); } @@ -223,9 +225,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(comment.AppliesTo.StringId); - responseDocument.SingleData.Attributes["archivedAt"].Should().BeCloseTo(comment.AppliesTo.ArchivedAt); + DateTimeOffset archivedAt = comment.AppliesTo.ArchivedAt.GetValueOrDefault(); + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(comment.AppliesTo.StringId); + responseDocument.Data.SingleValue.Attributes["archivedAt"].As().Should().BeCloseTo(archivedAt); } [Fact] @@ -250,9 +254,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); - responseDocument.ManyData[0].Attributes["archivedAt"].Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); + responseDocument.Data.ManyValue[0].Attributes["archivedAt"].Should().BeNull(); } [Fact] @@ -277,11 +281,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(station.Broadcasts.ElementAt(0).StringId); - responseDocument.ManyData[0].Attributes["archivedAt"].Should().BeCloseTo(station.Broadcasts.ElementAt(0).ArchivedAt); - responseDocument.ManyData[1].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); - responseDocument.ManyData[1].Attributes["archivedAt"].Should().BeNull(); + DateTimeOffset archivedAt0 = station.Broadcasts.ElementAt(0).ArchivedAt.GetValueOrDefault(); + + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(station.Broadcasts.ElementAt(0).StringId); + responseDocument.Data.ManyValue[0].Attributes["archivedAt"].As().Should().BeCloseTo(archivedAt0); + responseDocument.Data.ManyValue[1].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); + responseDocument.Data.ManyValue[1].Attributes["archivedAt"].Should().BeNull(); } [Fact] @@ -307,8 +313,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(network.Stations.ElementAt(0).StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(network.Stations.ElementAt(0).StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(network.Stations.ElementAt(0).Broadcasts.ElementAt(1).StringId); @@ -338,12 +344,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(network.Stations.ElementAt(0).StringId); + DateTimeOffset archivedAt0 = network.Stations.ElementAt(0).Broadcasts.ElementAt(0).ArchivedAt.GetValueOrDefault(); + + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(network.Stations.ElementAt(0).StringId); responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(network.Stations.ElementAt(0).Broadcasts.ElementAt(0).StringId); - responseDocument.Included[0].Attributes["archivedAt"].Should().BeCloseTo(network.Stations.ElementAt(0).Broadcasts.ElementAt(0).ArchivedAt); + responseDocument.Included[0].Attributes["archivedAt"].As().Should().BeCloseTo(archivedAt0); responseDocument.Included[1].Id.Should().Be(network.Stations.ElementAt(0).Broadcasts.ElementAt(1).StringId); responseDocument.Included[1].Attributes["archivedAt"].Should().BeNull(); } @@ -370,8 +378,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); } [Fact] @@ -396,9 +404,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(station.Broadcasts.ElementAt(0).StringId); - responseDocument.ManyData[1].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(station.Broadcasts.ElementAt(0).StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); } [Fact] @@ -428,10 +436,10 @@ public async Task Can_create_unarchived_resource() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["title"].Should().Be(newBroadcast.Title); - responseDocument.SingleData.Attributes["airedAt"].Should().BeCloseTo(newBroadcast.AiredAt); - responseDocument.SingleData.Attributes["archivedAt"].Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(newBroadcast.Title); + responseDocument.Data.SingleValue.Attributes["airedAt"].As().Should().BeCloseTo(newBroadcast.AiredAt); + responseDocument.Data.SingleValue.Attributes["archivedAt"].Should().BeNull(); } [Fact] @@ -457,14 +465,14 @@ public async Task Cannot_create_archived_resource() const string route = "/televisionBroadcasts"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Television broadcasts cannot be created in archived state."); error.Detail.Should().BeNull(); @@ -498,7 +506,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/televisionBroadcasts/" + existingBroadcast.StringId; + string route = $"/televisionBroadcasts/{existingBroadcast.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -541,7 +549,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/televisionBroadcasts/" + broadcast.StringId; + string route = $"/televisionBroadcasts/{broadcast.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -586,17 +594,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/televisionBroadcasts/" + broadcast.StringId; + string route = $"/televisionBroadcasts/{broadcast.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Archive date of television broadcasts cannot be shifted. Unarchive it first."); error.Detail.Should().BeNull(); @@ -614,7 +622,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/televisionBroadcasts/" + broadcast.StringId; + string route = $"/televisionBroadcasts/{broadcast.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -645,17 +653,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/televisionBroadcasts/" + broadcast.StringId; + string route = $"/televisionBroadcasts/{broadcast.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Television broadcasts must first be archived before they can be deleted."); error.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs index 10eed91f1b..862612d978 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs @@ -43,7 +43,7 @@ public override FilterExpression OnApplyFilter(FilterExpression existingFilter) if (IsReturningCollectionOfTelevisionBroadcasts() && !HasFilterOnArchivedAt(existingFilter)) { - AttrAttribute archivedAtAttribute = ResourceContext.Attributes.Single(attr => attr.Property.Name == nameof(TelevisionBroadcast.ArchivedAt)); + AttrAttribute archivedAtAttribute = ResourceContext.GetAttributeByPropertyName(nameof(TelevisionBroadcast.ArchivedAt)); var archivedAtChain = new ResourceFieldChainExpression(archivedAtAttribute); FilterExpression isUnarchived = new ComparisonExpression(ComparisonOperator.Equals, archivedAtChain, new NullConstantExpression()); @@ -151,7 +151,7 @@ private static void AssertIsNotArchived(TelevisionBroadcast broadcast) { if (broadcast.ArchivedAt != null) { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) + throw new JsonApiException(new ErrorObject(HttpStatusCode.Forbidden) { Title = "Television broadcasts cannot be created in archived state." }); @@ -163,7 +163,7 @@ private void AssertIsNotShiftingArchiveDate(TelevisionBroadcast broadcast) { if (_storedArchivedAt != null && broadcast.ArchivedAt != null && _storedArchivedAt != broadcast.ArchivedAt) { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) + throw new JsonApiException(new ErrorObject(HttpStatusCode.Forbidden) { Title = "Archive date of television broadcasts cannot be shifted. Unarchive it first." }); @@ -175,7 +175,7 @@ private static void AssertIsArchived(TelevisionBroadcast broadcast) { if (broadcast.ArchivedAt == null) { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) + throw new JsonApiException(new ErrorObject(HttpStatusCode.Forbidden) { Title = "Television broadcasts must first be archived before they can be deleted." }); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs index 3651c80368..d00cda4a39 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs @@ -62,8 +62,7 @@ public async Task Can_create_resources_for_matching_resource_type() const string route = "/operations/musicTracks/create"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -96,14 +95,14 @@ public async Task Cannot_create_resource_for_mismatching_resource_type() const string route = "/operations/musicTracks/create"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); @@ -144,14 +143,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations/musicTracks/create"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); @@ -199,14 +198,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations/musicTracks/create"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs index b4b45a4acb..5806456a9c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs @@ -40,11 +40,11 @@ private static void AssertOnlyCreatingMusicTracks(IEnumerable(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("performers"); - responseDocument.Results[0].SingleData.Attributes["artistName"].Should().Be(newArtistName); - responseDocument.Results[0].SingleData.Attributes["bornAt"].Should().BeCloseTo(newBornAt); - responseDocument.Results[0].SingleData.Relationships.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[0].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName); + responseDocument.Results[0].Data.SingleValue.Attributes["bornAt"].As().Should().BeCloseTo(newBornAt); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().BeNull(); - int newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -121,8 +120,7 @@ public async Task Can_create_resources() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -131,18 +129,18 @@ public async Task Can_create_resources() for (int index = 0; index < elementCount; index++) { - ResourceObject singleData = responseDocument.Results[index].SingleData; + ResourceObject singleData = responseDocument.Results[index].Data.SingleValue; singleData.Should().NotBeNull(); singleData.Type.Should().Be("musicTracks"); singleData.Attributes["title"].Should().Be(newTracks[index].Title); singleData.Attributes["lengthInSeconds"].As().Should().BeApproximately(newTracks[index].LengthInSeconds); singleData.Attributes["genre"].Should().Be(newTracks[index].Genre); - singleData.Attributes["releasedAt"].Should().BeCloseTo(newTracks[index].ReleasedAt); + singleData.Attributes["releasedAt"].As().Should().BeCloseTo(newTracks[index].ReleasedAt); singleData.Relationships.Should().NotBeEmpty(); } - Guid[] newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.SingleData.Id)).ToArray(); + Guid[] newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.Data.SingleValue.Id)).ToArray(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -190,20 +188,19 @@ public async Task Can_create_resource_without_attributes_or_relationships() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("performers"); - responseDocument.Results[0].SingleData.Attributes["artistName"].Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["bornAt"].Should().BeCloseTo(default(DateTimeOffset)); - responseDocument.Results[0].SingleData.Relationships.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[0].Data.SingleValue.Attributes["artistName"].Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["bornAt"].As().Should().BeCloseTo(default); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().BeNull(); - int newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -243,19 +240,18 @@ public async Task Can_create_resource_with_unknown_attribute() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("playlists"); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newName); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("playlists"); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(newName); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -285,8 +281,8 @@ public async Task Can_create_resource_with_unknown_relationship() { data = new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 } } } @@ -298,19 +294,18 @@ public async Task Can_create_resource_with_unknown_relationship() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("lyrics"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("lyrics"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - long newLyricId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newLyricId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -324,7 +319,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_resource_with_client_generated_ID() { // Arrange - string newTitle = _fakers.MusicTrack.Generate().Title; + MusicTrack newTrack = _fakers.MusicTrack.Generate(); + newTrack.Id = Guid.NewGuid(); var requestBody = new { @@ -336,10 +332,10 @@ public async Task Cannot_create_resource_with_client_generated_ID() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = newTrack.StringId, attributes = new { - title = newTitle + title = newTrack.Title } } } @@ -349,14 +345,14 @@ public async Task Cannot_create_resource_with_client_generated_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Specifying the resource ID in operations that create a resource is not allowed."); error.Detail.Should().BeNull(); @@ -382,14 +378,14 @@ public async Task Cannot_create_resource_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -418,14 +414,14 @@ public async Task Cannot_create_resource_for_ref_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.relationship' element is required."); error.Detail.Should().BeNull(); @@ -450,14 +446,14 @@ public async Task Cannot_create_resource_for_missing_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); @@ -488,14 +484,14 @@ public async Task Cannot_create_resource_for_missing_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); error.Detail.Should().BeNull(); @@ -515,7 +511,7 @@ public async Task Cannot_create_resource_for_unknown_type() op = "add", data = new { - type = "doesNotExist" + type = Unknown.ResourceType } } } @@ -524,17 +520,17 @@ public async Task Cannot_create_resource_for_unknown_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -569,14 +565,14 @@ public async Task Cannot_create_resource_for_array() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for create/update resource operation."); error.Detail.Should().BeNull(); @@ -609,14 +605,14 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Setting the initial value of the requested attribute is not allowed."); error.Detail.Should().Be("Setting the initial value of 'createdAt' is not allowed."); @@ -652,14 +648,14 @@ public async Task Cannot_create_resource_with_readonly_attribute() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isArchived' is read-only."); @@ -682,7 +678,7 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() type = "performers", attributes = new { - bornAt = "not-a-valid-time" + bornAt = 12345 } } } @@ -692,18 +688,18 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'DateTimeOffset'. - Request body:"); - error.Source.Pointer.Should().BeNull(); + error.Detail.Should().Be("Failed to convert attribute 'bornAt' with value '12345' of type 'Number' to type 'DateTimeOffset'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -774,19 +770,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTitle); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTitle); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs index 6a688dc325..eb3780a140 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs @@ -65,24 +65,25 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + string isoCode = $"{newLanguage.IsoCode}{ImplicitlyChangingTextLanguageDefinition.Suffix}"; + responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("textLanguages"); - responseDocument.Results[0].SingleData.Attributes["isoCode"].Should().Be(newLanguage.IsoCode + ImplicitlyChangingTextLanguageDefinition.Suffix); - responseDocument.Results[0].SingleData.Attributes.Should().NotContainKey("isRightToLeft"); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("textLanguages"); + responseDocument.Results[0].Data.SingleValue.Attributes["isoCode"].Should().Be(isoCode); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotContainKey("isRightToLeft"); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { TextLanguage languageInDatabase = await dbContext.TextLanguages.FirstWithIdAsync(newLanguage.Id); - languageInDatabase.IsoCode.Should().Be(newLanguage.IsoCode + ImplicitlyChangingTextLanguageDefinition.Suffix); + languageInDatabase.IsoCode.Should().Be(isoCode); }); } @@ -173,14 +174,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Another resource with the specified ID already exists."); error.Detail.Should().Be($"Another resource of type 'textLanguages' with ID '{languageToCreate.StringId}' already exists."); @@ -191,7 +192,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_resource_for_incompatible_ID() { // Arrange - string guid = Guid.NewGuid().ToString(); + string guid = Unknown.StringId.Guid; var requestBody = new { @@ -215,14 +216,14 @@ public async Task Cannot_create_resource_for_incompatible_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); @@ -243,7 +244,7 @@ public async Task Cannot_create_resource_for_ID_and_local_ID() data = new { type = "lyrics", - id = 12345678, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -253,14 +254,14 @@ public async Task Cannot_create_resource_for_ID_and_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index 01168cf1c8..d35107a8b8 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -82,19 +82,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -165,19 +164,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("playlists"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("playlists"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -212,7 +210,7 @@ public async Task Cannot_create_for_missing_relationship_type() { new { - id = 12345678 + id = Unknown.StringId.For() } } } @@ -225,14 +223,14 @@ public async Task Cannot_create_for_missing_relationship_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().Be("Expected 'type' element in 'performers' relationship."); @@ -261,8 +259,8 @@ public async Task Cannot_create_for_unknown_relationship_type() { new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -275,17 +273,17 @@ public async Task Cannot_create_for_unknown_relationship_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -324,14 +322,14 @@ public async Task Cannot_create_for_missing_relationship_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); @@ -344,6 +342,9 @@ public async Task Cannot_create_for_unknown_relationship_IDs() // Arrange string newTitle = _fakers.MusicTrack.Generate().Title; + string performerId1 = Unknown.StringId.For(); + string performerId2 = Unknown.StringId.AltFor(); + var requestBody = new { atomic__operations = new[] @@ -367,12 +368,12 @@ public async Task Cannot_create_for_unknown_relationship_IDs() new { type = "performers", - id = 12345678 + id = performerId1 }, new { type = "performers", - id = 87654321 + id = performerId2 } } } @@ -385,23 +386,23 @@ public async Task Cannot_create_for_unknown_relationship_IDs() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'performers' with ID '12345678' in relationship 'performers' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'performers' with ID '{performerId1}' in relationship 'performers' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'performers' with ID '87654321' in relationship 'performers' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'performers' with ID '{performerId2}' in relationship 'performers' does not exist."); error2.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -428,7 +429,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() new { type = "playlists", - id = 12345678 + id = Unknown.StringId.For() } } } @@ -441,14 +442,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); @@ -509,19 +510,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -561,14 +561,14 @@ public async Task Cannot_create_with_null_data_in_OneToMany_relationship() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -604,14 +604,14 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'tracks' relationship."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index f00959d331..9153bb404d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -3,11 +3,11 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; using TestBuildingBlocks; using Xunit; @@ -71,19 +71,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("lyrics"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("lyrics"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - long newLyricId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newLyricId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -140,19 +139,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -215,8 +213,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -225,12 +222,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => for (int index = 0; index < elementCount; index++) { - responseDocument.Results[index].SingleData.Should().NotBeNull(); - responseDocument.Results[index].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[index].SingleData.Attributes["title"].Should().Be(newTrackTitles[index]); + responseDocument.Results[index].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[index].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[index].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitles[index]); } - Guid[] newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.SingleData.Id)).ToArray(); + Guid[] newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.Data.SingleValue.Id)).ToArray(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -279,7 +276,7 @@ public async Task Cannot_create_for_missing_relationship_type() { data = new { - id = 12345678 + id = Unknown.StringId.For() } } } @@ -291,14 +288,14 @@ public async Task Cannot_create_for_missing_relationship_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().Be("Expected 'type' element in 'lyric' relationship."); @@ -325,8 +322,8 @@ public async Task Cannot_create_for_unknown_relationship_type() { data = new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -338,17 +335,17 @@ public async Task Cannot_create_for_unknown_relationship_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -384,14 +381,14 @@ public async Task Cannot_create_for_missing_relationship_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); @@ -402,6 +399,8 @@ public async Task Cannot_create_for_missing_relationship_ID() public async Task Cannot_create_with_unknown_relationship_ID() { // Arrange + string lyricId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -419,7 +418,7 @@ public async Task Cannot_create_with_unknown_relationship_ID() data = new { type = "lyrics", - id = 12345678 + id = lyricId } } } @@ -431,17 +430,17 @@ public async Task Cannot_create_with_unknown_relationship_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'lyrics' with ID '12345678' in relationship 'lyric' does not exist."); + error.Detail.Should().Be($"Related resource of type 'lyrics' with ID '{lyricId}' in relationship 'lyric' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -466,7 +465,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() data = new { type = "playlists", - id = 12345678 + id = Unknown.StringId.For() } } } @@ -478,14 +477,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); @@ -543,24 +542,23 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string requestBodyText = JsonConvert.SerializeObject(requestBody).Replace("ownedBy_duplicate", "ownedBy"); + string requestBodyText = JsonSerializer.Serialize(requestBody).Replace("ownedBy_duplicate", "ownedBy"); const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBodyText); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBodyText); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -594,7 +592,7 @@ public async Task Cannot_create_with_data_array_in_relationship() new { type = "lyrics", - id = 12345678 + id = Unknown.StringId.For() } } } @@ -607,14 +605,14 @@ public async Task Cannot_create_with_data_array_in_relationship() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index 53dd5ee603..ff0f20a15b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -347,14 +347,14 @@ public async Task Cannot_delete_resource_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -379,14 +379,14 @@ public async Task Cannot_delete_resource_for_missing_ref_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref' element is required."); error.Detail.Should().BeNull(); @@ -406,7 +406,7 @@ public async Task Cannot_delete_resource_for_missing_type() op = "remove", @ref = new { - id = 99999999 + id = Unknown.StringId.Int32 } } } @@ -415,14 +415,14 @@ public async Task Cannot_delete_resource_for_missing_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -442,8 +442,8 @@ public async Task Cannot_delete_resource_for_unknown_type() op = "remove", @ref = new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 } } } @@ -452,17 +452,17 @@ public async Task Cannot_delete_resource_for_unknown_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -488,14 +488,14 @@ public async Task Cannot_delete_resource_for_missing_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -506,6 +506,8 @@ public async Task Cannot_delete_resource_for_missing_ID() public async Task Cannot_delete_resource_for_unknown_ID() { // Arrange + string performerId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -516,7 +518,7 @@ public async Task Cannot_delete_resource_for_unknown_ID() @ref = new { type = "performers", - id = 99999999 + id = performerId } } } @@ -525,17 +527,17 @@ public async Task Cannot_delete_resource_for_unknown_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'performers' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'performers' with ID '{performerId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -543,7 +545,7 @@ public async Task Cannot_delete_resource_for_unknown_ID() public async Task Cannot_delete_resource_for_incompatible_ID() { // Arrange - string guid = Guid.NewGuid().ToString(); + string guid = Unknown.StringId.Guid; var requestBody = new { @@ -564,14 +566,14 @@ public async Task Cannot_delete_resource_for_incompatible_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int64'."); @@ -592,7 +594,7 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), lid = "local-1" } } @@ -602,14 +604,14 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs index 7e1eefbad6..3f5aadc2b3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs @@ -29,7 +29,7 @@ public override async Task OnWriteSucceededAsync(TextLanguage resource, WriteOpe { if (writeOperation is not WriteOperationKind.DeleteResource) { - string statement = "Update \"TextLanguages\" SET \"IsoCode\" = '" + resource.IsoCode + Suffix + "' WHERE \"Id\" = '" + resource.Id + "'"; + string statement = $"Update \"TextLanguages\" SET \"IsoCode\" = '{resource.IsoCode}{Suffix}' WHERE \"Id\" = '{resource.StringId}'"; await _dbContext.Database.ExecuteSqlRawAsync(statement, cancellationToken); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs index c33b0b4301..fb3246013a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs @@ -80,35 +80,34 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - string languageLink = HostPrefix + "/textLanguages/" + existingLanguage.StringId; + string languageLink = $"{HostPrefix}/textLanguages/{existingLanguage.StringId}"; - ResourceObject singleData1 = responseDocument.Results[0].SingleData; + ResourceObject singleData1 = responseDocument.Results[0].Data.SingleValue; singleData1.Should().NotBeNull(); singleData1.Links.Should().NotBeNull(); singleData1.Links.Self.Should().Be(languageLink); singleData1.Relationships.Should().NotBeEmpty(); singleData1.Relationships["lyrics"].Links.Should().NotBeNull(); - singleData1.Relationships["lyrics"].Links.Self.Should().Be(languageLink + "/relationships/lyrics"); - singleData1.Relationships["lyrics"].Links.Related.Should().Be(languageLink + "/lyrics"); + singleData1.Relationships["lyrics"].Links.Self.Should().Be($"{languageLink}/relationships/lyrics"); + singleData1.Relationships["lyrics"].Links.Related.Should().Be($"{languageLink}/lyrics"); - string companyLink = HostPrefix + "/recordCompanies/" + existingCompany.StringId; + string companyLink = $"{HostPrefix}/recordCompanies/{existingCompany.StringId}"; - ResourceObject singleData2 = responseDocument.Results[1].SingleData; + ResourceObject singleData2 = responseDocument.Results[1].Data.SingleValue; singleData2.Should().NotBeNull(); singleData2.Links.Should().NotBeNull(); singleData2.Links.Self.Should().Be(companyLink); singleData2.Relationships.Should().NotBeEmpty(); singleData2.Relationships["tracks"].Links.Should().NotBeNull(); - singleData2.Relationships["tracks"].Links.Self.Should().Be(companyLink + "/relationships/tracks"); - singleData2.Relationships["tracks"].Links.Related.Should().Be(companyLink + "/tracks"); + singleData2.Relationships["tracks"].Links.Self.Should().Be($"{companyLink}/relationships/tracks"); + singleData2.Relationships["tracks"].Links.Related.Should().Be($"{companyLink}/tracks"); } [Fact] @@ -145,15 +144,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - ResourceObject singleData = responseDocument.Results[0].SingleData; + ResourceObject singleData = responseDocument.Results[0].Data.SingleValue; singleData.Should().NotBeNull(); singleData.Links.Should().BeNull(); singleData.Relationships.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs index 0a685efa7e..176b7162dc 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs @@ -70,35 +70,34 @@ public async Task Create_resource_with_side_effects_returns_relative_links() const string route = "/api/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); - string languageLink = "/api/textLanguages/" + Guid.Parse(responseDocument.Results[0].SingleData.Id); + string languageLink = $"/api/textLanguages/{Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id)}"; - responseDocument.Results[0].SingleData.Links.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Links.Self.Should().Be(languageLink); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Self.Should().Be(languageLink + "/relationships/lyrics"); - responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Related.Should().Be(languageLink + "/lyrics"); + responseDocument.Results[0].Data.SingleValue.Links.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Links.Self.Should().Be(languageLink); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships["lyrics"].Links.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Relationships["lyrics"].Links.Self.Should().Be($"{languageLink}/relationships/lyrics"); + responseDocument.Results[0].Data.SingleValue.Relationships["lyrics"].Links.Related.Should().Be($"{languageLink}/lyrics"); - responseDocument.Results[1].SingleData.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); - string companyLink = "/api/recordCompanies/" + short.Parse(responseDocument.Results[1].SingleData.Id); + string companyLink = $"/api/recordCompanies/{short.Parse(responseDocument.Results[1].Data.SingleValue.Id)}"; - responseDocument.Results[1].SingleData.Links.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Links.Self.Should().Be(companyLink); - responseDocument.Results[1].SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Self.Should().Be(companyLink + "/relationships/tracks"); - responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Related.Should().Be(companyLink + "/tracks"); + responseDocument.Results[1].Data.SingleValue.Links.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Links.Self.Should().Be(companyLink); + responseDocument.Results[1].Data.SingleValue.Relationships.Should().NotBeEmpty(); + responseDocument.Results[1].Data.SingleValue.Relationships["tracks"].Links.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Relationships["tracks"].Links.Self.Should().Be($"{companyLink}/relationships/tracks"); + responseDocument.Results[1].Data.SingleValue.Relationships["tracks"].Links.Related.Should().Be($"{companyLink}/tracks"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs index 219d21a808..036cb640af 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs @@ -79,27 +79,26 @@ public async Task Can_create_resource_with_ManyToOne_relationship_using_local_ID const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("recordCompanies"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newCompany.Name); - responseDocument.Results[0].SingleData.Attributes["countryOfResidence"].Should().Be(newCompany.CountryOfResidence); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("recordCompanies"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(newCompany.Name); + responseDocument.Results[0].Data.SingleValue.Attributes["countryOfResidence"].Should().Be(newCompany.CountryOfResidence); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - short newCompanyId = short.Parse(responseDocument.Results[0].SingleData.Id); - Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + short newCompanyId = short.Parse(responseDocument.Results[0].Data.SingleValue.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -173,27 +172,26 @@ public async Task Can_create_resource_with_OneToMany_relationship_using_local_ID const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("performers"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["artistName"].Should().Be(newPerformer.ArtistName); - responseDocument.Results[0].SingleData.Attributes["bornAt"].Should().BeCloseTo(newPerformer.BornAt); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["artistName"].Should().Be(newPerformer.ArtistName); + responseDocument.Results[0].Data.SingleValue.Attributes["bornAt"].As().Should().BeCloseTo(newPerformer.BornAt); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - int newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); - Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[0].Data.SingleValue.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -266,26 +264,25 @@ public async Task Can_create_resource_with_ManyToMany_relationship_using_local_I const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("playlists"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["name"].Should().Be(newPlaylistName); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("playlists"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["name"].Should().Be(newPlaylistName); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - long newPlaylistId = long.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); + long newPlaylistId = long.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -315,7 +312,7 @@ public async Task Cannot_consume_local_ID_that_is_assigned_in_same_operation() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -344,14 +341,14 @@ public async Task Cannot_consume_local_ID_that_is_assigned_in_same_operation() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Local ID cannot be both defined and used within the same operation."); error.Detail.Should().Be("Local ID 'company-1' cannot be both defined and used within the same operation."); @@ -375,7 +372,7 @@ public async Task Cannot_reassign_local_ID() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -410,14 +407,14 @@ public async Task Cannot_reassign_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Another local ID with the same name is already defined at this point."); error.Detail.Should().Be("Another local ID with name 'playlist-1' is already defined at this point."); @@ -469,23 +466,22 @@ public async Task Can_update_resource_using_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[0].SingleData.Attributes["genre"].Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Attributes["genre"].Should().BeNull(); - responseDocument.Results[1].Data.Should().BeNull(); + responseDocument.Results[1].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -588,34 +584,33 @@ public async Task Can_update_resource_with_relationships_using_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(4); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("performers"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["artistName"].Should().Be(newArtistName); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName); - responseDocument.Results[2].SingleData.Should().NotBeNull(); - responseDocument.Results[2].SingleData.Type.Should().Be("recordCompanies"); - responseDocument.Results[2].SingleData.Lid.Should().BeNull(); - responseDocument.Results[2].SingleData.Attributes["name"].Should().Be(newCompanyName); + responseDocument.Results[2].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[2].Data.SingleValue.Type.Should().Be("recordCompanies"); + responseDocument.Results[2].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[2].Data.SingleValue.Attributes["name"].Should().Be(newCompanyName); - responseDocument.Results[3].Data.Should().BeNull(); + responseDocument.Results[3].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - int newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); - short newCompanyId = short.Parse(responseDocument.Results[2].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); + int newPerformerId = int.Parse(responseDocument.Results[1].Data.SingleValue.Id); + short newCompanyId = short.Parse(responseDocument.Results[2].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -702,28 +697,27 @@ public async Task Can_create_ManyToOne_relationship_using_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(3); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("recordCompanies"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["name"].Should().Be(newCompanyName); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("recordCompanies"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["name"].Should().Be(newCompanyName); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - short newCompanyId = short.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); + short newCompanyId = short.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -801,28 +795,27 @@ public async Task Can_create_OneToMany_relationship_using_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(3); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("performers"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["artistName"].Should().Be(newArtistName); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - int newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); + int newPerformerId = int.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -900,28 +893,27 @@ public async Task Can_create_ManyToMany_relationship_using_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(3); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("playlists"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newPlaylistName); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("playlists"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(newPlaylistName); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); - Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1021,28 +1013,27 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(3); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("performers"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["artistName"].Should().Be(newArtistName); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - int newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); + int newPerformerId = int.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1142,28 +1133,27 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(3); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("playlists"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newPlaylistName); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("playlists"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(newPlaylistName); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); - Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1263,28 +1253,27 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(3); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("performers"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["artistName"].Should().Be(newArtistName); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - int newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); + int newPerformerId = int.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1406,30 +1395,29 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(4); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("playlists"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newPlaylistName); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("playlists"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(newPlaylistName); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - responseDocument.Results[3].Data.Should().BeNull(); + responseDocument.Results[3].Data.Value.Should().BeNull(); - long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); - Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1560,32 +1548,31 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(4); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("performers"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["artistName"].Should().Be(newArtistName1); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName1); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("performers"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["artistName"].Should().Be(newArtistName2); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName2); - responseDocument.Results[2].SingleData.Should().NotBeNull(); - responseDocument.Results[2].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[2].SingleData.Lid.Should().BeNull(); - responseDocument.Results[2].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[2].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[2].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[2].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[2].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[3].Data.Should().BeNull(); + responseDocument.Results[3].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[2].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[2].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1693,24 +1680,23 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(4); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].Data.Should().BeNull(); + responseDocument.Results[1].Data.Value.Should().BeNull(); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - responseDocument.Results[3].Data.Should().BeNull(); + responseDocument.Results[3].Data.Value.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1761,22 +1747,21 @@ public async Task Can_delete_resource_using_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].Data.Should().BeNull(); + responseDocument.Results[1].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1800,7 +1785,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_ref() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -1809,7 +1794,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_ref() @ref = new { type = "musicTracks", - lid = "doesNotExist" + lid = Unknown.LocalId } } } @@ -1818,17 +1803,17 @@ public async Task Cannot_consume_unassigned_local_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); - error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Detail.Should().Be($"Server-generated value for local ID '{Unknown.LocalId}' is not available at this point."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); } @@ -1846,7 +1831,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_data_element() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -1855,7 +1840,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_data_element() data = new { type = "musicTracks", - lid = "doesNotExist", + lid = Unknown.LocalId, attributes = new { } @@ -1867,17 +1852,17 @@ public async Task Cannot_consume_unassigned_local_ID_in_data_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); - error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Detail.Should().Be($"Server-generated value for local ID '{Unknown.LocalId}' is not available at this point."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); } @@ -1903,7 +1888,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -1920,7 +1905,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "performers", - lid = "doesNotExist" + lid = Unknown.LocalId } } } @@ -1930,17 +1915,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); - error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Detail.Should().Be($"Server-generated value for local ID '{Unknown.LocalId}' is not available at this point."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); } @@ -1958,7 +1943,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_elemen @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -1974,7 +1959,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_elemen data = new { type = "recordCompanies", - lid = "doesNotExist" + lid = Unknown.LocalId } } } @@ -1986,17 +1971,17 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_elemen const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); - error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Detail.Should().Be($"Server-generated value for local ID '{Unknown.LocalId}' is not available at this point."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); } @@ -2014,7 +1999,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_array( @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2032,7 +2017,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_array( new { type = "musicTracks", - lid = "doesNotExist" + lid = Unknown.LocalId } } } @@ -2045,17 +2030,17 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_array( const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); - error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Detail.Should().Be($"Server-generated value for local ID '{Unknown.LocalId}' is not available at this point."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); } @@ -2075,7 +2060,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_same_operation() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2104,14 +2089,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_same_operation() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'track-1' belongs to resource type 'musicTracks' instead of 'recordCompanies'."); @@ -2134,7 +2119,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_ref() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2161,14 +2146,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'company-1' belongs to resource type 'recordCompanies' instead of 'musicTracks'."); @@ -2191,7 +2176,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_data_element() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2221,14 +2206,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_data_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'performer-1' belongs to resource type 'performers' instead of 'playlists'."); @@ -2259,7 +2244,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2295,14 +2280,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'company-1' belongs to resource type 'recordCompanies' instead of 'performers'."); @@ -2327,7 +2312,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2368,14 +2353,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'playlist-1' belongs to resource type 'playlists' instead of 'recordCompanies'."); @@ -2398,7 +2383,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2438,14 +2423,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'performer-1' belongs to resource type 'performers' instead of 'musicTracks'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs index 8c6dc402ec..a13c92eb2c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Extensions; @@ -79,19 +80,18 @@ public async Task Returns_resource_meta_in_create_resource_with_side_effects() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Meta.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Meta["Copyright"].Should().Be("(C) 2018. All rights reserved."); + responseDocument.Results[0].Data.SingleValue.Meta.Should().HaveCount(1); + ((JsonElement)responseDocument.Results[0].Data.SingleValue.Meta["copyright"]).GetString().Should().Be("(C) 2018. All rights reserved."); - responseDocument.Results[1].SingleData.Meta.Should().HaveCount(1); - responseDocument.Results[1].SingleData.Meta["Copyright"].Should().Be("(C) 1994. All rights reserved."); + responseDocument.Results[1].Data.SingleValue.Meta.Should().HaveCount(1); + ((JsonElement)responseDocument.Results[1].Data.SingleValue.Meta["copyright"]).GetString().Should().Be("(C) 1994. All rights reserved."); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -136,15 +136,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Meta.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Meta["Notice"].Should().Be(TextLanguageMetaDefinition.NoticeText); + responseDocument.Results[0].Data.SingleValue.Meta.Should().HaveCount(1); + ((JsonElement)responseDocument.Results[0].Data.SingleValue.Meta["notice"]).GetString().Should().Be(TextLanguageMetaDefinition.NoticeText); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs index 41b1f43567..f775ea6c79 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs @@ -1,14 +1,13 @@ -using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json.Linq; using TestBuildingBlocks; using Xunit; @@ -58,17 +57,16 @@ public async Task Returns_top_level_meta_in_create_resource_with_side_effects() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.Should().HaveCount(3); - responseDocument.Meta["license"].Should().Be("MIT"); - responseDocument.Meta["projectUrl"].Should().Be("https://github.com/json-api-dotnet/JsonApiDotNetCore/"); + ((JsonElement)responseDocument.Meta["license"]).GetString().Should().Be("MIT"); + ((JsonElement)responseDocument.Meta["projectUrl"]).GetString().Should().Be("https://github.com/json-api-dotnet/JsonApiDotNetCore/"); - string[] versionArray = ((IEnumerable)responseDocument.Meta["versions"]).Select(token => token.ToString()).ToArray(); + string[] versionArray = ((JsonElement)responseDocument.Meta["versions"]).EnumerateArray().Select(element => element.GetString()).ToArray(); versionArray.Should().HaveCount(4); versionArray.Should().Contain("v4.0.0"); @@ -111,17 +109,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.Should().HaveCount(3); - responseDocument.Meta["license"].Should().Be("MIT"); - responseDocument.Meta["projectUrl"].Should().Be("https://github.com/json-api-dotnet/JsonApiDotNetCore/"); + ((JsonElement)responseDocument.Meta["license"]).GetString().Should().Be("MIT"); + ((JsonElement)responseDocument.Meta["projectUrl"]).GetString().Should().Be("https://github.com/json-api-dotnet/JsonApiDotNetCore/"); - string[] versionArray = ((IEnumerable)responseDocument.Meta["versions"]).Select(token => token.ToString()).ToArray(); + string[] versionArray = ((JsonElement)responseDocument.Meta["versions"]).EnumerateArray().Select(element => element.GetString()).ToArray(); versionArray.Should().HaveCount(4); versionArray.Should().Contain("v4.0.0"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs index 42e18e0ba4..8b2a56a092 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs @@ -29,18 +29,18 @@ public async Task Cannot_process_for_missing_request_body() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, null); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, null); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); - error.Source.Pointer.Should().BeNull(); + error.Source.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -61,18 +61,18 @@ public async Task Cannot_process_for_broken_JSON_request_body() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Unexpected end of content while loading JObject."); - error.Source.Pointer.Should().BeNull(); + error.Detail.Should().Match("* There is an open JSON object or array that should be closed. *"); + error.Source.Should().BeNull(); } [Fact] @@ -87,18 +87,18 @@ public async Task Cannot_process_empty_operations_array() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: No operations found."); error.Detail.Should().BeNull(); - error.Source.Pointer.Should().BeNull(); + error.Source.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -135,18 +135,18 @@ public async Task Cannot_process_for_unknown_operation_code() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Error converting value \"merge\" to type"); - error.Source.Pointer.Should().BeNull(); + error.Detail.Should().StartWith("The JSON value could not be converted to "); + error.Source.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs index a47557ab7d..bc9b637617 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs @@ -1,3 +1,4 @@ +using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -12,6 +13,8 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations.Mixed { public sealed class AtomicSerializationTests : IClassFixture, OperationsDbContext>> { + private const string JsonDateTimeOffsetFormatSpecifier = "yyyy-MM-ddTHH:mm:ss.FFFFFFFK"; + private readonly IntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new(); @@ -30,6 +33,7 @@ public AtomicSerializationTests(IntegrationTestContext(); + options.IncludeExceptionStackTraceInErrors = false; options.IncludeJsonApiVersion = true; options.AllowClientGeneratedIds = true; } @@ -38,8 +42,8 @@ public AtomicSerializationTests(IntegrationTestContext { @@ -56,10 +60,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "performers", - id = newArtistId, + id = newPerformer.StringId, attributes = new { - artistName = newArtistName + artistName = newPerformer.ArtistName, + bornAt = newPerformer.BornAt } } } @@ -85,14 +90,67 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { ""data"": { ""type"": ""performers"", - ""id"": """ + newArtistId + @""", + ""id"": """ + newPerformer.StringId + @""", ""attributes"": { - ""artistName"": """ + newArtistName + @""", - ""bornAt"": ""0001-01-01T01:00:00+01:00"" + ""artistName"": """ + newPerformer.ArtistName + @""", + ""bornAt"": """ + newPerformer.BornAt.ToString(JsonDateTimeOffsetFormatSpecifier) + @""" }, ""links"": { - ""self"": ""http://localhost/performers/" + newArtistId + @""" + ""self"": ""http://localhost/performers/" + newPerformer.StringId + @""" + } + } + } + ] +}"); } + + [Fact] + public async Task Includes_version_with_ext_on_error_in_operations_endpoint() + { + // Arrange + string musicTrackId = Unknown.StringId.For(); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "remove", + @ref = new + { + type = "musicTracks", + id = musicTrackId + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + + string errorId = JsonApiStringConverter.ExtractErrorId(responseDocument); + + responseDocument.Should().BeJson(@"{ + ""jsonapi"": { + ""version"": ""1.1"", + ""ext"": [ + ""https://jsonapi.org/ext/atomic"" + ] + }, + ""errors"": [ + { + ""id"": """ + errorId + @""", + ""status"": ""404"", + ""title"": ""The requested resource does not exist."", + ""detail"": ""Resource of type 'musicTracks' with ID '" + musicTrackId + @"' does not exist."", + ""source"": { + ""pointer"": ""/atomic:operations[0]"" } } ] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs index a48c2424cd..d72a79f9b8 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs @@ -38,6 +38,7 @@ public async Task Cannot_process_more_operations_than_maximum() op = "add", data = new { + type = "performers" } }, new @@ -45,6 +46,7 @@ public async Task Cannot_process_more_operations_than_maximum() op = "remove", data = new { + type = "performers" } }, new @@ -52,6 +54,7 @@ public async Task Cannot_process_more_operations_than_maximum() op = "update", data = new { + type = "performers" } } } @@ -60,18 +63,18 @@ public async Task Cannot_process_more_operations_than_maximum() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request exceeds the maximum number of operations."); error.Detail.Should().Be("The number of operations in this request (3) is higher than 2."); - error.Source.Pointer.Should().BeNull(); + error.Source.Should().BeNull(); } [Fact] @@ -113,7 +116,7 @@ public async Task Can_process_operations_same_as_maximum() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -153,7 +156,7 @@ public async Task Can_process_high_number_of_operations_when_unconstrained() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs index 706c990642..39b43ecf01 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs @@ -49,20 +49,20 @@ public async Task Cannot_create_resource_with_multiple_violations() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Title field is required."); error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/title"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); @@ -118,15 +118,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -173,20 +172,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Title field is required."); error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/title"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); @@ -432,7 +431,7 @@ public async Task Validates_all_operations_before_execution_starts() data = new { type = "playlists", - id = 99999999, + id = Unknown.StringId.For(), attributes = new { name = (string)null @@ -457,26 +456,26 @@ public async Task Validates_all_operations_before_execution_starts() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(3); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Name field is required."); error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/name"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The Title field is required."); error2.Source.Pointer.Should().Be("/atomic:operations[1]/data/attributes/title"); - Error error3 = responseDocument.Errors[2]; + ErrorObject error3 = responseDocument.Errors[2]; error3.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error3.Title.Should().Be("Input validation failed."); error3.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs index 3c19514876..cd63f41dd1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs @@ -37,10 +37,6 @@ public AtomicQueryStringTests(IntegrationTestContext(); }); - - var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); - options.AllowQueryStringOverrideForSerializerDefaultValueHandling = true; - options.AllowQueryStringOverrideForSerializerNullValueHandling = true; } [Fact] @@ -68,14 +64,14 @@ public async Task Cannot_include_on_operations_endpoint() const string route = "/operations?include=recordCompanies"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'include' cannot be used at this endpoint."); @@ -107,14 +103,14 @@ public async Task Cannot_filter_on_operations_endpoint() const string route = "/operations?filter=equals(id,'1')"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'filter' cannot be used at this endpoint."); @@ -146,14 +142,14 @@ public async Task Cannot_sort_on_operations_endpoint() const string route = "/operations?sort=-id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'sort' cannot be used at this endpoint."); @@ -185,14 +181,14 @@ public async Task Cannot_use_pagination_number_on_operations_endpoint() const string route = "/operations?page[number]=1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'page[number]' cannot be used at this endpoint."); @@ -224,14 +220,14 @@ public async Task Cannot_use_pagination_size_on_operations_endpoint() const string route = "/operations?page[size]=1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'page[size]' cannot be used at this endpoint."); @@ -263,14 +259,14 @@ public async Task Cannot_use_sparse_fieldset_on_operations_endpoint() const string route = "/operations?fields[recordCompanies]=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'fields[recordCompanies]' cannot be used at this endpoint."); @@ -301,8 +297,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(musicTracks[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(musicTracks[2].StringId); } [Fact] @@ -333,14 +329,14 @@ public async Task Cannot_use_Queryable_handler_on_operations_endpoint() const string route = "/operations?isRecentlyReleased=true"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Unknown query string parameter."); @@ -349,93 +345,5 @@ public async Task Cannot_use_Queryable_handler_on_operations_endpoint() error.Source.Parameter.Should().Be("isRecentlyReleased"); } - - [Fact] - public async Task Can_use_defaults_on_operations_endpoint() - { - // Arrange - string newTrackTitle = _fakers.MusicTrack.Generate().Title; - decimal? newTrackLength = _fakers.MusicTrack.Generate().LengthInSeconds; - - var requestBody = new - { - atomic__operations = new object[] - { - new - { - op = "add", - data = new - { - type = "musicTracks", - attributes = new - { - title = newTrackTitle, - lengthInSeconds = newTrackLength - } - } - } - } - }; - - const string route = "/operations?defaults=false"; - - // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[0].SingleData.Attributes["lengthInSeconds"].As().Should().BeApproximately(newTrackLength); - } - - [Fact] - public async Task Can_use_nulls_on_operations_endpoint() - { - // Arrange - string newTrackTitle = _fakers.MusicTrack.Generate().Title; - decimal? newTrackLength = _fakers.MusicTrack.Generate().LengthInSeconds; - - var requestBody = new - { - atomic__operations = new object[] - { - new - { - op = "add", - data = new - { - type = "musicTracks", - attributes = new - { - title = newTrackTitle, - lengthInSeconds = newTrackLength - } - } - } - } - }; - - const string route = "/operations?nulls=false"; - - // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[0].SingleData.Attributes["lengthInSeconds"].As().Should().BeApproximately(newTrackLength); - } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs index 33a9055cf1..fbde96e586 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs @@ -86,19 +86,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newCompanies[0].Name.ToUpperInvariant()); - responseDocument.Results[0].SingleData.Attributes["countryOfResidence"].Should().Be(newCompanies[0].CountryOfResidence.ToUpperInvariant()); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(newCompanies[0].Name.ToUpperInvariant()); + responseDocument.Results[0].Data.SingleValue.Attributes["countryOfResidence"].Should().Be(newCompanies[0].CountryOfResidence.ToUpperInvariant()); - responseDocument.Results[1].SingleData.Attributes["name"].Should().Be(newCompanies[1].Name.ToUpperInvariant()); - responseDocument.Results[1].SingleData.Attributes["countryOfResidence"].Should().Be(newCompanies[1].CountryOfResidence.ToUpperInvariant()); + responseDocument.Results[1].Data.SingleValue.Attributes["name"].Should().Be(newCompanies[1].Name.ToUpperInvariant()); + responseDocument.Results[1].Data.SingleValue.Attributes["countryOfResidence"].Should().Be(newCompanies[1].CountryOfResidence.ToUpperInvariant()); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -170,8 +169,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -230,19 +228,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Results.Should().HaveCount(2); + string country0 = existingCompanies[0].CountryOfResidence.ToUpperInvariant(); + string country1 = existingCompanies[1].CountryOfResidence.ToUpperInvariant(); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(existingCompanies[0].Name); - responseDocument.Results[0].SingleData.Attributes["countryOfResidence"].Should().Be(existingCompanies[0].CountryOfResidence.ToUpperInvariant()); + responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[1].SingleData.Attributes["name"].Should().Be(existingCompanies[1].Name); - responseDocument.Results[1].SingleData.Attributes["countryOfResidence"].Should().Be(existingCompanies[1].CountryOfResidence.ToUpperInvariant()); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(existingCompanies[0].Name); + responseDocument.Results[0].Data.SingleValue.Attributes["countryOfResidence"].Should().Be(country0); + responseDocument.Results[1].Data.SingleValue.Attributes["name"].Should().Be(existingCompanies[1].Name); + responseDocument.Results[1].Data.SingleValue.Attributes["countryOfResidence"].Should().Be(country1); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -313,8 +312,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs index 620a0b638e..4c6df1fc48 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs @@ -84,19 +84,18 @@ public async Task Hides_text_in_create_resource_with_side_effects() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Attributes["format"].Should().Be(newLyrics[0].Format); - responseDocument.Results[0].SingleData.Attributes.Should().NotContainKey("text"); + responseDocument.Results[0].Data.SingleValue.Attributes["format"].Should().Be(newLyrics[0].Format); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotContainKey("text"); - responseDocument.Results[1].SingleData.Attributes["format"].Should().Be(newLyrics[1].Format); - responseDocument.Results[1].SingleData.Attributes.Should().NotContainKey("text"); + responseDocument.Results[1].Data.SingleValue.Attributes["format"].Should().Be(newLyrics[1].Format); + responseDocument.Results[1].Data.SingleValue.Attributes.Should().NotContainKey("text"); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -158,19 +157,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Attributes["format"].Should().Be(existingLyrics[0].Format); - responseDocument.Results[0].SingleData.Attributes.Should().NotContainKey("text"); + responseDocument.Results[0].Data.SingleValue.Attributes["format"].Should().Be(existingLyrics[0].Format); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotContainKey("text"); - responseDocument.Results[1].SingleData.Attributes["format"].Should().Be(existingLyrics[1].Format); - responseDocument.Results[1].SingleData.Attributes.Should().NotContainKey("text"); + responseDocument.Results[1].Data.SingleValue.Attributes["format"].Should().Be(existingLyrics[1].Format); + responseDocument.Results[1].Data.SingleValue.Attributes.Should().NotContainKey("text"); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs index b6ff66af7a..542a6971b3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs @@ -36,6 +36,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.ClearTablesAsync(); }); + string performerId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new object[] @@ -72,7 +74,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "performers", - id = 99999999 + id = performerId } } } @@ -85,17 +87,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'performers' with ID '99999999' in relationship 'performers' does not exist."); + error.Detail.Should().Be($"Related resource of type 'performers' with ID '{performerId}' in relationship 'performers' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); await _testContext.RunOnDatabaseAsync(async dbContext => diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs index 8029a7481f..245af3c761 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs @@ -60,14 +60,14 @@ public async Task Cannot_use_non_transactional_repository() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported resource type in atomic:operations request."); error.Detail.Should().Be("Operations on resources of type 'performers' cannot be used because transaction support is unavailable."); @@ -99,14 +99,14 @@ public async Task Cannot_use_transactional_repository_without_active_transaction const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of resource types in atomic:operations request."); error.Detail.Should().Be("All operations need to participate in a single shared transaction, which is not the case for this request."); @@ -138,14 +138,14 @@ public async Task Cannot_use_distributed_transaction() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of resource types in atomic:operations request."); error.Detail.Should().Be("All operations need to participate in a single shared transaction, which is not the case for this request."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index c095e6ec7e..ada44f47e7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -4,7 +4,6 @@ using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; using TestBuildingBlocks; @@ -62,14 +61,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted in 'add' operations."); error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); @@ -254,14 +253,14 @@ public async Task Cannot_add_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -281,7 +280,7 @@ public async Task Cannot_add_for_missing_type_in_ref() op = "add", @ref = new { - id = 99999999, + id = Unknown.StringId.For(), relationship = "tracks" } } @@ -291,14 +290,14 @@ public async Task Cannot_add_for_missing_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -318,8 +317,8 @@ public async Task Cannot_add_for_unknown_type_in_ref() op = "add", @ref = new { - type = "doesNotExist", - id = 99999999, + type = Unknown.ResourceType, + id = Unknown.StringId.For(), relationship = "tracks" } } @@ -329,17 +328,17 @@ public async Task Cannot_add_for_unknown_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -366,14 +365,14 @@ public async Task Cannot_add_for_missing_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -392,6 +391,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string companyId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -402,7 +403,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => @ref = new { type = "recordCompanies", - id = 9999, + id = companyId, relationship = "tracks" }, data = new[] @@ -420,17 +421,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'recordCompanies' with ID '9999' does not exist."); + error.Detail.Should().Be($"Resource of type 'recordCompanies' with ID '{companyId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -448,7 +449,7 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), lid = "local-1", relationship = "performers" } @@ -459,14 +460,14 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -486,8 +487,8 @@ public async Task Cannot_add_for_missing_relationship_in_ref() op = "add", @ref = new { - id = 99999999, - type = "musicTracks" + type = "musicTracks", + id = Unknown.StringId.For() } } } @@ -496,14 +497,14 @@ public async Task Cannot_add_for_missing_relationship_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.relationship' element is required."); error.Detail.Should().BeNull(); @@ -524,8 +525,8 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() @ref = new { type = "performers", - id = 99999999, - relationship = "doesNotExist" + id = Unknown.StringId.For(), + relationship = Unknown.Relationship } } } @@ -534,17 +535,17 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); - error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'performers' does not contain a relationship named '{Unknown.Relationship}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -581,14 +582,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -609,14 +610,14 @@ public async Task Cannot_add_for_missing_type_in_data() @ref = new { type = "playlists", - id = 99999999, + id = Unknown.StringId.For(), relationship = "tracks" }, data = new[] { new { - id = Guid.NewGuid().ToString() + id = Unknown.StringId.For() } } } @@ -626,14 +627,14 @@ public async Task Cannot_add_for_missing_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); error.Detail.Should().BeNull(); @@ -654,15 +655,15 @@ public async Task Cannot_add_for_unknown_type_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -672,17 +673,17 @@ public async Task Cannot_add_for_unknown_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -700,7 +701,7 @@ public async Task Cannot_add_for_missing_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] @@ -717,14 +718,14 @@ public async Task Cannot_add_for_missing_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -745,7 +746,7 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] @@ -753,7 +754,7 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() new { type = "performers", - id = 99999999, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -764,14 +765,14 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -783,7 +784,12 @@ public async Task Cannot_add_for_unknown_IDs_in_data() { // Arrange RecordCompany existingCompany = _fakers.RecordCompany.Generate(); - Guid[] trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); + + string[] trackIds = + { + Unknown.StringId.For(), + Unknown.StringId.AltFor() + }; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -809,12 +815,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "musicTracks", - id = trackIds[0].ToString() + id = trackIds[0] }, new { type = "musicTracks", - id = trackIds[1].ToString() + id = trackIds[1] } } } @@ -824,20 +830,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); @@ -874,7 +880,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "playlists", - id = 88888888 + id = Unknown.StringId.For() } } } @@ -884,14 +890,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); error.Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index 627dfe404e..ffaff765c3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -4,7 +4,6 @@ using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; using TestBuildingBlocks; @@ -63,14 +62,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted in 'remove' operations."); error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); @@ -254,14 +253,14 @@ public async Task Cannot_remove_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -281,7 +280,7 @@ public async Task Cannot_remove_for_missing_type_in_ref() op = "remove", @ref = new { - id = 99999999, + id = Unknown.StringId.For(), relationship = "tracks" } } @@ -291,14 +290,14 @@ public async Task Cannot_remove_for_missing_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -318,8 +317,8 @@ public async Task Cannot_remove_for_unknown_type_in_ref() op = "remove", @ref = new { - type = "doesNotExist", - id = 99999999, + type = Unknown.ResourceType, + id = Unknown.StringId.For(), relationship = "tracks" } } @@ -329,17 +328,17 @@ public async Task Cannot_remove_for_unknown_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -366,14 +365,14 @@ public async Task Cannot_remove_for_missing_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -392,6 +391,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string companyId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -402,7 +403,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => @ref = new { type = "recordCompanies", - id = 9999, + id = companyId, relationship = "tracks" }, data = new[] @@ -420,17 +421,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'recordCompanies' with ID '9999' does not exist."); + error.Detail.Should().Be($"Resource of type 'recordCompanies' with ID '{companyId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -448,7 +449,7 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), lid = "local-1", relationship = "performers" } @@ -459,14 +460,14 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -487,8 +488,8 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() @ref = new { type = "performers", - id = 99999999, - relationship = "doesNotExist" + id = Unknown.StringId.For(), + relationship = Unknown.Relationship } } } @@ -497,17 +498,17 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); - error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'performers' does not contain a relationship named '{Unknown.Relationship}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -544,14 +545,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -572,14 +573,14 @@ public async Task Cannot_remove_for_missing_type_in_data() @ref = new { type = "playlists", - id = 99999999, + id = Unknown.StringId.For(), relationship = "tracks" }, data = new[] { new { - id = Guid.NewGuid().ToString() + id = Unknown.StringId.For() } } } @@ -589,14 +590,14 @@ public async Task Cannot_remove_for_missing_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); error.Detail.Should().BeNull(); @@ -617,15 +618,15 @@ public async Task Cannot_remove_for_unknown_type_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -635,17 +636,17 @@ public async Task Cannot_remove_for_unknown_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -663,7 +664,7 @@ public async Task Cannot_remove_for_missing_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] @@ -680,14 +681,14 @@ public async Task Cannot_remove_for_missing_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -708,7 +709,7 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] @@ -716,7 +717,7 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() new { type = "performers", - id = 99999999, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -727,14 +728,14 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -746,7 +747,12 @@ public async Task Cannot_remove_for_unknown_IDs_in_data() { // Arrange RecordCompany existingCompany = _fakers.RecordCompany.Generate(); - Guid[] trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); + + string[] trackIds = + { + Unknown.StringId.For(), + Unknown.StringId.AltFor() + }; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -772,12 +778,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "musicTracks", - id = trackIds[0].ToString() + id = trackIds[0] }, new { type = "musicTracks", - id = trackIds[1].ToString() + id = trackIds[1] } } } @@ -787,20 +793,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); @@ -837,7 +843,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "playlists", - id = 88888888 + id = Unknown.StringId.For() } } } @@ -847,14 +853,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); error.Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index 69c02d0fa6..c1426db8a9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -4,7 +4,6 @@ using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; using TestBuildingBlocks; @@ -291,14 +290,14 @@ public async Task Cannot_replace_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -318,7 +317,7 @@ public async Task Cannot_replace_for_missing_type_in_ref() op = "update", @ref = new { - id = 99999999, + id = Unknown.StringId.For(), relationship = "tracks" } } @@ -328,14 +327,14 @@ public async Task Cannot_replace_for_missing_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -355,8 +354,8 @@ public async Task Cannot_replace_for_unknown_type_in_ref() op = "update", @ref = new { - type = "doesNotExist", - id = 99999999, + type = Unknown.ResourceType, + id = Unknown.StringId.For(), relationship = "tracks" } } @@ -366,17 +365,17 @@ public async Task Cannot_replace_for_unknown_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -403,14 +402,14 @@ public async Task Cannot_replace_for_missing_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -429,6 +428,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string companyId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -439,7 +440,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => @ref = new { type = "recordCompanies", - id = 9999, + id = companyId, relationship = "tracks" }, data = new[] @@ -457,17 +458,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'recordCompanies' with ID '9999' does not exist."); + error.Detail.Should().Be($"Resource of type 'recordCompanies' with ID '{companyId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -475,7 +476,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_replace_for_incompatible_ID_in_ref() { // Arrange - string guid = Guid.NewGuid().ToString(); + string guid = Unknown.StringId.Guid; MusicTrack existingTrack = _fakers.MusicTrack.Generate(); @@ -513,14 +514,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int16'."); @@ -541,7 +542,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), lid = "local-1", relationship = "performers" } @@ -552,14 +553,14 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -580,8 +581,8 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() @ref = new { type = "performers", - id = 99999999, - relationship = "doesNotExist" + id = Unknown.StringId.For(), + relationship = Unknown.Relationship } } } @@ -590,17 +591,17 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); - error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'performers' does not contain a relationship named '{Unknown.Relationship}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -637,14 +638,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -665,14 +666,14 @@ public async Task Cannot_replace_for_missing_type_in_data() @ref = new { type = "playlists", - id = 99999999, + id = Unknown.StringId.For(), relationship = "tracks" }, data = new[] { new { - id = Guid.NewGuid().ToString() + id = Unknown.StringId.For() } } } @@ -682,14 +683,14 @@ public async Task Cannot_replace_for_missing_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); error.Detail.Should().BeNull(); @@ -710,15 +711,15 @@ public async Task Cannot_replace_for_unknown_type_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -728,17 +729,17 @@ public async Task Cannot_replace_for_unknown_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -756,7 +757,7 @@ public async Task Cannot_replace_for_missing_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] @@ -773,14 +774,14 @@ public async Task Cannot_replace_for_missing_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -801,7 +802,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] @@ -809,7 +810,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() new { type = "performers", - id = 99999999, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -820,14 +821,14 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -839,7 +840,12 @@ public async Task Cannot_replace_for_unknown_IDs_in_data() { // Arrange RecordCompany existingCompany = _fakers.RecordCompany.Generate(); - Guid[] trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); + + string[] trackIds = + { + Unknown.StringId.For(), + Unknown.StringId.AltFor() + }; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -865,12 +871,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "musicTracks", - id = trackIds[0].ToString() + id = trackIds[0] }, new { type = "musicTracks", - id = trackIds[1].ToString() + id = trackIds[1] } } } @@ -880,20 +886,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); @@ -940,14 +946,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); @@ -984,7 +990,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "playlists", - id = 88888888 + id = Unknown.StringId.For() } } } @@ -994,14 +1000,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); error.Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 5471ab1add..85c153a7f1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -537,14 +537,14 @@ public async Task Cannot_create_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -564,7 +564,7 @@ public async Task Cannot_create_for_missing_type_in_ref() op = "update", @ref = new { - id = 99999999, + id = Unknown.StringId.For(), relationship = "track" } } @@ -574,14 +574,14 @@ public async Task Cannot_create_for_missing_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -601,8 +601,8 @@ public async Task Cannot_create_for_unknown_type_in_ref() op = "update", @ref = new { - type = "doesNotExist", - id = 99999999, + type = Unknown.ResourceType, + id = Unknown.StringId.For(), relationship = "ownedBy" } } @@ -612,17 +612,17 @@ public async Task Cannot_create_for_unknown_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -649,14 +649,14 @@ public async Task Cannot_create_for_missing_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -667,7 +667,7 @@ public async Task Cannot_create_for_missing_ID_in_ref() public async Task Cannot_create_for_unknown_ID_in_ref() { // Arrange - string missingTrackId = Guid.NewGuid().ToString(); + string trackId = Unknown.StringId.For(); Lyric existingLyric = _fakers.Lyric.Generate(); @@ -687,7 +687,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => @ref = new { type = "musicTracks", - id = missingTrackId, + id = trackId, relationship = "lyric" }, data = new @@ -702,17 +702,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be($"Resource of type 'musicTracks' with ID '{missingTrackId}' does not exist."); + error.Detail.Should().Be($"Resource of type 'musicTracks' with ID '{trackId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -753,14 +753,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); @@ -781,7 +781,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_ref() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), lid = "local-1", relationship = "ownedBy" } @@ -792,14 +792,14 @@ public async Task Cannot_create_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -820,8 +820,8 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() @ref = new { type = "performers", - id = 99999999, - relationship = "doesNotExist" + id = Unknown.StringId.For(), + relationship = Unknown.Relationship } } } @@ -830,17 +830,17 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); - error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'performers' does not contain a relationship named '{Unknown.Relationship}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -874,7 +874,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } } } @@ -884,14 +884,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); @@ -912,12 +912,12 @@ public async Task Cannot_create_for_missing_type_in_data() @ref = new { type = "lyrics", - id = 99999999, + id = Unknown.StringId.For(), relationship = "track" }, data = new { - id = Guid.NewGuid().ToString() + id = Unknown.StringId.For() } } } @@ -926,14 +926,14 @@ public async Task Cannot_create_for_missing_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); error.Detail.Should().BeNull(); @@ -954,13 +954,13 @@ public async Task Cannot_create_for_unknown_type_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "lyric" }, data = new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -969,17 +969,17 @@ public async Task Cannot_create_for_unknown_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -997,7 +997,7 @@ public async Task Cannot_create_for_missing_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "lyric" }, data = new @@ -1011,14 +1011,14 @@ public async Task Cannot_create_for_missing_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); @@ -1039,13 +1039,13 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "lyric" }, data = new { type = "lyrics", - id = 99999999, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -1055,14 +1055,14 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); @@ -1081,6 +1081,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string lyricId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -1097,7 +1099,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "lyrics", - id = 99999999 + id = lyricId } } } @@ -1106,17 +1108,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'lyrics' with ID '99999999' in relationship 'lyric' does not exist."); + error.Detail.Should().Be($"Related resource of type 'lyrics' with ID '{lyricId}' in relationship 'lyric' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1157,14 +1159,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); @@ -1199,7 +1201,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "playlists", - id = 99999999 + id = Unknown.StringId.For() } } } @@ -1208,14 +1210,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data.type' element."); error.Detail.Should().Be("Expected resource of type 'lyrics' in 'data.type', instead of 'playlists'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 736ffd8646..3ee9307112 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -4,7 +4,6 @@ using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; using TestBuildingBlocks; @@ -330,14 +329,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -358,7 +357,7 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() data = new { type = "playlists", - id = 99999999, + id = Unknown.StringId.For(), relationships = new { tracks = new @@ -367,7 +366,7 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() { new { - id = Guid.NewGuid().ToString() + id = Unknown.StringId.For() } } } @@ -380,14 +379,14 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().Be("Expected 'type' element in 'tracks' relationship."); @@ -408,7 +407,7 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationships = new { performers = new @@ -417,8 +416,8 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -431,17 +430,17 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -459,7 +458,7 @@ public async Task Cannot_replace_for_missing_ID_in_relationship_data() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationships = new { performers = new @@ -481,14 +480,14 @@ public async Task Cannot_replace_for_missing_ID_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); @@ -509,7 +508,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationships = new { performers = new @@ -519,7 +518,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() new { type = "performers", - id = 99999999, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -533,14 +532,14 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); @@ -552,7 +551,12 @@ public async Task Cannot_replace_for_unknown_IDs_in_relationship_data() { // Arrange RecordCompany existingCompany = _fakers.RecordCompany.Generate(); - Guid[] trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); + + string[] trackIds = + { + Unknown.StringId.For(), + Unknown.StringId.AltFor() + }; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -580,12 +584,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "musicTracks", - id = trackIds[0].ToString() + id = trackIds[0] }, new { type = "musicTracks", - id = trackIds[1].ToString() + id = trackIds[1] } } } @@ -598,20 +602,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); @@ -650,7 +654,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "playlists", - id = 88888888 + id = Unknown.StringId.For() } } } @@ -663,14 +667,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 391d2bafd6..1a6b73e239 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -238,8 +238,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 } } } @@ -419,24 +419,23 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("textLanguages"); - responseDocument.Results[0].SingleData.Attributes["isoCode"].Should().Be(newIsoCode + ImplicitlyChangingTextLanguageDefinition.Suffix); - responseDocument.Results[0].SingleData.Attributes.Should().NotContainKey("isRightToLeft"); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("textLanguages"); + responseDocument.Results[0].Data.SingleValue.Attributes["isoCode"].Should().Be($"{newIsoCode}{ImplicitlyChangingTextLanguageDefinition.Suffix}"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotContainKey("isRightToLeft"); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { TextLanguage languageInDatabase = await dbContext.TextLanguages.FirstWithIdAsync(existingLanguage.Id); - languageInDatabase.IsoCode.Should().Be(newIsoCode + ImplicitlyChangingTextLanguageDefinition.Suffix); + languageInDatabase.IsoCode.Should().Be($"{newIsoCode}{ImplicitlyChangingTextLanguageDefinition.Suffix}"); }); } @@ -472,16 +471,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Values.Should().OnlyContain(relationshipEntry => relationshipEntry.Data == null); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Values.Should().OnlyContain(relationshipObject => relationshipObject.Data.Value == null); } [Fact] @@ -503,14 +501,14 @@ public async Task Cannot_update_resource_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -587,12 +585,12 @@ public async Task Cannot_update_resource_for_missing_type_in_ref() op = "update", @ref = new { - id = 12345678 + id = Unknown.StringId.For() }, data = new { type = "performers", - id = 12345678, + id = Unknown.StringId.For(), attributes = new { }, @@ -607,14 +605,14 @@ public async Task Cannot_update_resource_for_missing_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -639,7 +637,7 @@ public async Task Cannot_update_resource_for_missing_ID_in_ref() data = new { type = "performers", - id = 12345678, + id = Unknown.StringId.For(), attributes = new { }, @@ -654,14 +652,14 @@ public async Task Cannot_update_resource_for_missing_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -682,13 +680,13 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_ref() @ref = new { type = "performers", - id = 12345678, + id = Unknown.StringId.For(), lid = "local-1" }, data = new { type = "performers", - id = 12345678, + id = Unknown.StringId.AltFor(), attributes = new { }, @@ -703,14 +701,14 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -735,14 +733,14 @@ public async Task Cannot_update_resource_for_missing_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); @@ -762,7 +760,7 @@ public async Task Cannot_update_resource_for_missing_type_in_data() op = "update", data = new { - id = 12345678, + id = Unknown.StringId.Int32, attributes = new { }, @@ -777,14 +775,14 @@ public async Task Cannot_update_resource_for_missing_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); error.Detail.Should().BeNull(); @@ -819,14 +817,14 @@ public async Task Cannot_update_resource_for_missing_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); @@ -847,7 +845,7 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() data = new { type = "performers", - id = 12345678, + id = Unknown.StringId.For(), lid = "local-1", attributes = new { @@ -863,14 +861,14 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); @@ -915,14 +913,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for create/update resource operation."); error.Detail.Should().BeNull(); @@ -943,12 +941,12 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() @ref = new { type = "performers", - id = 12345678 + id = Unknown.StringId.For() }, data = new { type = "playlists", - id = 12345678, + id = Unknown.StringId.For(), attributes = new { }, @@ -963,14 +961,14 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.type' and 'data.type' element."); error.Detail.Should().Be("Expected resource of type 'performers' in 'data.type', instead of 'playlists'."); @@ -981,6 +979,9 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() { // Arrange + string performerId1 = Unknown.StringId.For(); + string performerId2 = Unknown.StringId.AltFor(); + var requestBody = new { atomic__operations = new[] @@ -991,12 +992,12 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() @ref = new { type = "performers", - id = 12345678 + id = performerId1 }, data = new { type = "performers", - id = 87654321, + id = performerId2, attributes = new { }, @@ -1011,17 +1012,17 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource ID mismatch between 'ref.id' and 'data.id' element."); - error.Detail.Should().Be("Expected resource with ID '12345678' in 'data.id', instead of '87654321'."); + error.Detail.Should().Be($"Expected resource with ID '{performerId1}' in 'data.id', instead of '{performerId2}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1059,14 +1060,14 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource local ID mismatch between 'ref.lid' and 'data.lid' element."); error.Detail.Should().Be("Expected resource with local ID 'local-1' in 'data.lid', instead of 'local-2'."); @@ -1077,6 +1078,8 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_data() { // Arrange + string performerId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -1087,7 +1090,7 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da @ref = new { type = "performers", - id = "12345678" + id = performerId }, data = new { @@ -1107,17 +1110,17 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource identity mismatch between 'ref.id' and 'data.lid' element."); - error.Detail.Should().Be("Expected resource with ID '12345678' in 'data.id', instead of 'local-1' in 'data.lid'."); + error.Detail.Should().Be($"Expected resource with ID '{performerId}' in 'data.id', instead of 'local-1' in 'data.lid'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1125,6 +1128,8 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_data() { // Arrange + string performerId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -1140,7 +1145,7 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da data = new { type = "performers", - id = "12345678", + id = performerId, attributes = new { }, @@ -1155,17 +1160,17 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource identity mismatch between 'ref.lid' and 'data.id' element."); - error.Detail.Should().Be("Expected resource with local ID 'local-1' in 'data.lid', instead of '12345678' in 'data.id'."); + error.Detail.Should().Be($"Expected resource with local ID 'local-1' in 'data.lid', instead of '{performerId}' in 'data.id'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1182,8 +1187,8 @@ public async Task Cannot_update_resource_for_unknown_type() op = "update", data = new { - type = "doesNotExist", - id = 12345678, + type = Unknown.ResourceType, + id = Unknown.StringId.Int32, attributes = new { }, @@ -1198,17 +1203,17 @@ public async Task Cannot_update_resource_for_unknown_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1216,6 +1221,8 @@ public async Task Cannot_update_resource_for_unknown_type() public async Task Cannot_update_resource_for_unknown_ID() { // Arrange + string performerId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -1226,7 +1233,7 @@ public async Task Cannot_update_resource_for_unknown_ID() data = new { type = "performers", - id = 99999999, + id = performerId, attributes = new { }, @@ -1241,17 +1248,17 @@ public async Task Cannot_update_resource_for_unknown_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'performers' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'performers' with ID '{performerId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1259,7 +1266,7 @@ public async Task Cannot_update_resource_for_unknown_ID() public async Task Cannot_update_resource_for_incompatible_ID() { // Arrange - string guid = Guid.NewGuid().ToString(); + string guid = Unknown.StringId.Guid; var requestBody = new { @@ -1288,14 +1295,14 @@ public async Task Cannot_update_resource_for_incompatible_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); @@ -1337,14 +1344,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Changing the value of the requested attribute is not allowed."); error.Detail.Should().Be("Changing the value of 'createdAt' is not allowed."); @@ -1386,14 +1393,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isArchived' is read-only."); @@ -1435,17 +1442,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); - error.Detail.Should().BeNull(); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().Be("Resource ID is read-only."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1474,7 +1481,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingPerformer.StringId, attributes = new { - bornAt = "not-a-valid-time" + bornAt = 123.45 } } } @@ -1484,18 +1491,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'DateTimeOffset'. - Request body:"); - error.Source.Pointer.Should().BeNull(); + error.Detail.Should().Be("Failed to convert attribute 'bornAt' with value '123.45' of type 'Number' to type 'DateTimeOffset'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index f65a58f9c3..9487a69551 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -595,7 +595,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } } } @@ -608,14 +608,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); @@ -636,14 +636,14 @@ public async Task Cannot_create_for_missing_type_in_relationship_data() data = new { type = "lyrics", - id = 99999999, + id = Unknown.StringId.For(), relationships = new { track = new { data = new { - id = Guid.NewGuid().ToString() + id = Unknown.StringId.For() } } } @@ -655,14 +655,14 @@ public async Task Cannot_create_for_missing_type_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().Be("Expected 'type' element in 'track' relationship."); @@ -683,15 +683,15 @@ public async Task Cannot_create_for_unknown_type_in_relationship_data() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationships = new { lyric = new { data = new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -703,17 +703,17 @@ public async Task Cannot_create_for_unknown_type_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -731,7 +731,7 @@ public async Task Cannot_create_for_missing_ID_in_relationship_data() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationships = new { lyric = new @@ -750,14 +750,14 @@ public async Task Cannot_create_for_missing_ID_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); @@ -778,7 +778,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationships = new { lyric = new @@ -786,7 +786,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() data = new { type = "lyrics", - id = 99999999, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -799,14 +799,14 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); @@ -825,6 +825,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string lyricId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -843,7 +845,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "lyrics", - id = 99999999 + id = lyricId } } } @@ -855,17 +857,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'lyrics' with ID '99999999' in relationship 'lyric' does not exist."); + error.Detail.Should().Be($"Related resource of type 'lyrics' with ID '{lyricId}' in relationship 'lyric' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -899,7 +901,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "playlists", - id = 99999999 + id = Unknown.StringId.For() } } } @@ -911,14 +913,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs index 2703102568..cce9320cce 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs @@ -23,12 +23,12 @@ internal sealed class CarExpressionRewriter : QueryExpressionRewriter private readonly AttrAttribute _regionIdAttribute; private readonly AttrAttribute _licensePlateAttribute; - public CarExpressionRewriter(IResourceContextProvider resourceContextProvider) + public CarExpressionRewriter(IResourceGraph resourceGraph) { - ResourceContext carResourceContext = resourceContextProvider.GetResourceContext(); + ResourceContext carResourceContext = resourceGraph.GetResourceContext(); - _regionIdAttribute = carResourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Car.RegionId)); - _licensePlateAttribute = carResourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Car.LicensePlate)); + _regionIdAttribute = carResourceContext.GetAttributeByPropertyName(nameof(Car.RegionId)); + _licensePlateAttribute = carResourceContext.GetAttributeByPropertyName(nameof(Car.LicensePlate)); } public override QueryExpression VisitComparison(ComparisonExpression expression, object argument) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs index 979bab3c02..1eff765ccd 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs @@ -60,8 +60,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(car.StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(car.StringId); } [Fact] @@ -81,7 +81,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/cars/" + car.StringId; + string route = $"/cars/{car.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -89,8 +89,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(car.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(car.StringId); } [Fact] @@ -118,8 +118,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(car.StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(car.StringId); } [Fact] @@ -147,8 +147,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(car.StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(car.StringId); } [Fact] @@ -234,7 +234,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/engines/" + existingEngine.StringId; + string route = $"/engines/{existingEngine.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -290,7 +290,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/engines/" + existingEngine.StringId; + string route = $"/engines/{existingEngine.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -527,14 +527,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/dealerships/{existingDealership.StringId}/relationships/inventory"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be("Related resource of type 'cars' with ID '999:XX-YY-22' in relationship 'inventory' does not exist."); @@ -557,7 +557,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/cars/" + existingCar.StringId; + string route = $"/cars/{existingCar.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs index 02ec68055d..cca76a2e53 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs @@ -63,7 +63,7 @@ public async Task Permits_no_Accept_headers_at_operations_endpoint() const string contentType = HeaderConstants.AtomicOperationsMediaType; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -116,10 +116,10 @@ public async Task Permits_JsonApi_without_parameters_in_Accept_headers() Action setRequestHeaders = headers => { headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; ext=other")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; q=0.3")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; profile=some")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; ext=other")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; unknown=unexpected")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; q=0.3")); }; // Act @@ -158,14 +158,14 @@ public async Task Permits_JsonApi_with_AtomicOperations_extension_in_Accept_head Action setRequestHeaders = headers => { headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; profile=some")); headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType)); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + ";ext=\"https://jsonapi.org/ext/atomic\"; q=0.2")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; unknown=unexpected")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType};ext=\"https://jsonapi.org/ext/atomic\"; q=0.2")); }; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType, setRequestHeaders); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType, setRequestHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -180,24 +180,25 @@ public async Task Denies_JsonApi_with_parameters_in_Accept_headers() Action setRequestHeaders = headers => { headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; ext=other")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; profile=some")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; ext=other")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; unknown=unexpected")); headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.AtomicOperationsMediaType)); }; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route, setRequestHeaders); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route, setRequestHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotAcceptable); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotAcceptable); error.Title.Should().Be("The specified Accept header value does not contain any supported media types."); error.Detail.Should().Be("Please include 'application/vnd.api+json' in the Accept header values."); + error.Source.Header.Should().Be("Accept"); } [Fact] @@ -232,18 +233,19 @@ public async Task Denies_JsonApi_in_Accept_headers_at_operations_endpoint() }; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType, setRequestHeaders); + (HttpResponseMessage httpResponse, Document responseDocument) = + await _testContext.ExecutePostAsync(route, requestBody, contentType, setRequestHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotAcceptable); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotAcceptable); error.Title.Should().Be("The specified Accept header value does not contain any supported media types."); error.Detail.Should().Be("Please include 'application/vnd.api+json; ext=\"https://jsonapi.org/ext/atomic\"' in the Accept header values."); + error.Source.Header.Should().Be("Accept"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs index 3ba8975183..1ae336c663 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs @@ -88,18 +88,18 @@ public async Task Denies_unknown_ContentType_header() const string contentType = "text/html"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'text/html' for the Content-Type header value."); + error.Source.Header.Should().Be("Content-Type"); } [Fact] @@ -123,7 +123,7 @@ public async Task Permits_JsonApi_ContentType_header() // Act // ReSharper disable once RedundantArgumentDefaultValue - (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -156,7 +156,7 @@ public async Task Permits_JsonApi_ContentType_header_with_AtomicOperations_exten const string contentType = HeaderConstants.AtomicOperationsMediaType; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -179,21 +179,21 @@ public async Task Denies_JsonApi_ContentType_header_with_profile() }; const string route = "/policies"; - const string contentType = HeaderConstants.MediaType + "; profile=something"; + string contentType = $"{HeaderConstants.MediaType}; profile=something"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); + error.Source.Header.Should().Be("Content-Type"); } [Fact] @@ -213,21 +213,21 @@ public async Task Denies_JsonApi_ContentType_header_with_extension() }; const string route = "/policies"; - const string contentType = HeaderConstants.MediaType + "; ext=something"; + string contentType = $"{HeaderConstants.MediaType}; ext=something"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); + error.Source.Header.Should().Be("Content-Type"); } [Fact] @@ -250,18 +250,18 @@ public async Task Denies_JsonApi_ContentType_header_with_AtomicOperations_extens const string contentType = HeaderConstants.AtomicOperationsMediaType; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); + error.Source.Header.Should().Be("Content-Type"); } [Fact] @@ -281,21 +281,21 @@ public async Task Denies_JsonApi_ContentType_header_with_CharSet() }; const string route = "/policies"; - const string contentType = HeaderConstants.MediaType + "; charset=ISO-8859-4"; + string contentType = $"{HeaderConstants.MediaType}; charset=ISO-8859-4"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); + error.Source.Header.Should().Be("Content-Type"); } [Fact] @@ -315,21 +315,21 @@ public async Task Denies_JsonApi_ContentType_header_with_unknown_parameter() }; const string route = "/policies"; - const string contentType = HeaderConstants.MediaType + "; unknown=unexpected"; + string contentType = $"{HeaderConstants.MediaType}; unknown=unexpected"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); + error.Source.Header.Should().Be("Content-Type"); } [Fact] @@ -360,8 +360,7 @@ public async Task Denies_JsonApi_ContentType_header_at_operations_endpoint() // Act // ReSharper disable once RedundantArgumentDefaultValue - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); @@ -370,10 +369,11 @@ public async Task Denies_JsonApi_ContentType_header_at_operations_endpoint() string detail = $"Please specify '{HeaderConstants.AtomicOperationsMediaType}' instead of '{contentType}' for the Content-Type header value."; - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be(detail); + error.Source.Header.Should().Be("Content-Type"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs index a467b0ed25..51bb1c6f5c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs @@ -31,7 +31,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/toothbrushes/" + toothbrush.StringId; + string route = $"/toothbrushes/{toothbrush.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -39,25 +39,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(toothbrush.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(toothbrush.StringId); } [Fact] public async Task Converts_empty_ActionResult_to_error_collection() { // Arrange - string route = "/toothbrushes/" + BaseToothbrushesController.EmptyActionResultId; + string route = $"/toothbrushes/{BaseToothbrushesController.EmptyActionResultId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("NotFound"); error.Detail.Should().BeNull(); @@ -67,17 +67,17 @@ public async Task Converts_empty_ActionResult_to_error_collection() public async Task Converts_ActionResult_with_error_object_to_error_collection() { // Arrange - string route = "/toothbrushes/" + BaseToothbrushesController.ActionResultWithErrorObjectId; + string route = $"/toothbrushes/{BaseToothbrushesController.ActionResultWithErrorObjectId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("No toothbrush with that ID exists."); error.Detail.Should().BeNull(); @@ -87,17 +87,17 @@ public async Task Converts_ActionResult_with_error_object_to_error_collection() public async Task Cannot_convert_ActionResult_with_string_parameter_to_error_collection() { // Arrange - string route = "/toothbrushes/" + BaseToothbrushesController.ActionResultWithStringParameter; + string route = $"/toothbrushes/{BaseToothbrushesController.ActionResultWithStringParameter}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); error.Title.Should().Be("An unhandled error occurred while processing this request."); error.Detail.Should().Be("Data being returned must be errors or resources."); @@ -107,17 +107,17 @@ public async Task Cannot_convert_ActionResult_with_string_parameter_to_error_col public async Task Converts_ObjectResult_with_error_object_to_error_collection() { // Arrange - string route = "/toothbrushes/" + BaseToothbrushesController.ObjectResultWithErrorObjectId; + string route = $"/toothbrushes/{BaseToothbrushesController.ObjectResultWithErrorObjectId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadGateway); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadGateway); error.Title.Should().BeNull(); error.Detail.Should().BeNull(); @@ -127,27 +127,27 @@ public async Task Converts_ObjectResult_with_error_object_to_error_collection() public async Task Converts_ObjectResult_with_error_objects_to_error_collection() { // Arrange - string route = "/toothbrushes/" + BaseToothbrushesController.ObjectResultWithErrorCollectionId; + string route = $"/toothbrushes/{BaseToothbrushesController.ObjectResultWithErrorCollectionId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(3); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.PreconditionFailed); error1.Title.Should().BeNull(); error1.Detail.Should().BeNull(); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.Unauthorized); error2.Title.Should().BeNull(); error2.Detail.Should().BeNull(); - Error error3 = responseDocument.Errors[2]; + ErrorObject error3 = responseDocument.Errors[2]; error3.StatusCode.Should().Be(HttpStatusCode.ExpectationFailed); error3.Title.Should().Be("This is not a very great request."); error3.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs index 001ada5fb0..4718323321 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs @@ -32,7 +32,7 @@ public override async Task GetAsync(int id, CancellationToken can if (id == ActionResultWithErrorObjectId) { - return NotFound(new Error(HttpStatusCode.NotFound) + return NotFound(new ErrorObject(HttpStatusCode.NotFound) { Title = "No toothbrush with that ID exists." }); @@ -45,16 +45,16 @@ public override async Task GetAsync(int id, CancellationToken can if (id == ObjectResultWithErrorObjectId) { - return Error(new Error(HttpStatusCode.BadGateway)); + return Error(new ErrorObject(HttpStatusCode.BadGateway)); } if (id == ObjectResultWithErrorCollectionId) { var errors = new[] { - new Error(HttpStatusCode.PreconditionFailed), - new Error(HttpStatusCode.Unauthorized), - new Error(HttpStatusCode.ExpectationFailed) + new ErrorObject(HttpStatusCode.PreconditionFailed), + new ErrorObject(HttpStatusCode.Unauthorized), + new ErrorObject(HttpStatusCode.ExpectationFailed) { Title = "This is not a very great request." } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs index ee3d9e2882..f49c18ad9d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs @@ -26,14 +26,14 @@ public async Task ApiController_attribute_transforms_NotFound_action_result_with const string route = "/world-civilians/missing"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.Links.About.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.4"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs index 6565f2074a..05b7c0214d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs @@ -38,7 +38,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/world-api/civilization/popular/towns/" + town.StringId; + string route = $"/world-api/civilization/popular/towns/{town.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -46,16 +46,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("towns"); - responseDocument.SingleData.Id.Should().Be(town.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(town.Name); - responseDocument.SingleData.Attributes["latitude"].Should().Be(town.Latitude); - responseDocument.SingleData.Attributes["longitude"].Should().Be(town.Longitude); - responseDocument.SingleData.Relationships["civilians"].Links.Self.Should().Be(HostPrefix + route + "/relationships/civilians"); - responseDocument.SingleData.Relationships["civilians"].Links.Related.Should().Be(HostPrefix + route + "/civilians"); - responseDocument.SingleData.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("towns"); + responseDocument.Data.SingleValue.Id.Should().Be(town.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(town.Name); + responseDocument.Data.SingleValue.Attributes["latitude"].Should().Be(town.Latitude); + responseDocument.Data.SingleValue.Attributes["longitude"].Should().Be(town.Longitude); + responseDocument.Data.SingleValue.Relationships["civilians"].Links.Self.Should().Be($"{HostPrefix}{route}/relationships/civilians"); + responseDocument.Data.SingleValue.Relationships["civilians"].Links.Related.Should().Be($"{HostPrefix}{route}/civilians"); + responseDocument.Data.SingleValue.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); } [Fact] @@ -79,10 +79,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(5); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Type == "towns"); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Attributes.Any()); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Relationships.Any()); + responseDocument.Data.ManyValue.Should().HaveCount(5); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Type == "towns"); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Attributes.Any()); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Relationships.Any()); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs index 994ce5dbb0..0e0482dbce 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs @@ -44,7 +44,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/buildings/" + building.StringId; + string route = $"/buildings/{building.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -52,12 +52,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(building.StringId); - responseDocument.SingleData.Attributes["number"].Should().Be(building.Number); - responseDocument.SingleData.Attributes["windowCount"].Should().Be(4); - responseDocument.SingleData.Attributes["primaryDoorColor"].Should().Be(building.PrimaryDoor.Color); - responseDocument.SingleData.Attributes["secondaryDoorColor"].Should().Be(building.SecondaryDoor.Color); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(building.StringId); + responseDocument.Data.SingleValue.Attributes["number"].Should().Be(building.Number); + responseDocument.Data.SingleValue.Attributes["windowCount"].Should().Be(4); + responseDocument.Data.SingleValue.Attributes["primaryDoorColor"].Should().Be(building.PrimaryDoor.Color); + responseDocument.Data.SingleValue.Attributes["secondaryDoorColor"].Should().Be(building.SecondaryDoor.Color); } [Fact] @@ -80,7 +80,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/streets/" + street.StringId; + string route = $"/streets/{street.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -88,12 +88,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(street.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(street.Name); - responseDocument.SingleData.Attributes["buildingCount"].Should().Be(2); - responseDocument.SingleData.Attributes["doorTotalCount"].Should().Be(3); - responseDocument.SingleData.Attributes["windowTotalCount"].Should().Be(5); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(street.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(street.Name); + responseDocument.Data.SingleValue.Attributes["buildingCount"].Should().Be(2); + responseDocument.Data.SingleValue.Attributes["doorTotalCount"].Should().Be(3); + responseDocument.Data.SingleValue.Attributes["windowTotalCount"].Should().Be(5); } [Fact] @@ -119,11 +119,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(street.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["windowTotalCount"].Should().Be(3); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(street.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["windowTotalCount"].Should().Be(3); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } [Fact] @@ -151,9 +151,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(state.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(state.Name); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(state.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(state.Name); responseDocument.Included.Should().HaveCount(2); @@ -194,11 +194,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(state.Cities[0].StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["name"].Should().Be(state.Cities[0].Name); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(state.Cities[0].StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["name"].Should().Be(state.Cities[0].Name); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("streets"); @@ -235,13 +235,13 @@ public async Task Can_create_resource() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["number"].Should().Be(newBuilding.Number); - responseDocument.SingleData.Attributes["windowCount"].Should().Be(0); - responseDocument.SingleData.Attributes["primaryDoorColor"].Should().BeNull(); - responseDocument.SingleData.Attributes["secondaryDoorColor"].Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["number"].Should().Be(newBuilding.Number); + responseDocument.Data.SingleValue.Attributes["windowCount"].Should().Be(0); + responseDocument.Data.SingleValue.Attributes["primaryDoorColor"].Should().BeNull(); + responseDocument.Data.SingleValue.Attributes["secondaryDoorColor"].Should().BeNull(); - int newBuildingId = int.Parse(responseDocument.SingleData.Id); + int newBuildingId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -297,7 +297,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/buildings/" + existingBuilding.StringId; + string route = $"/buildings/{existingBuilding.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -343,7 +343,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/buildings/" + existingBuilding.StringId; + string route = $"/buildings/{existingBuilding.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/AlternateExceptionHandler.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/AlternateExceptionHandler.cs index e728b74230..d6af489f76 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/AlternateExceptionHandler.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/AlternateExceptionHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Serialization.Objects; @@ -23,12 +24,14 @@ protected override LogLevel GetLogLevel(Exception exception) return base.GetLogLevel(exception); } - protected override ErrorDocument CreateErrorDocument(Exception exception) + protected override Document CreateErrorDocument(Exception exception) { if (exception is ConsumerArticleIsNoLongerAvailableException articleException) { - articleException.Errors[0].Meta.Data.Add("support", - $"Please contact us for info about similar articles at {articleException.SupportEmailAddress}."); + articleException.Errors[0].Meta = new Dictionary + { + ["Support"] = $"Please contact us for info about similar articles at {articleException.SupportEmailAddress}." + }; } return base.CreateErrorDocument(exception); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ConsumerArticleIsNoLongerAvailableException.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ConsumerArticleIsNoLongerAvailableException.cs index bbf635822d..78260204d7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ConsumerArticleIsNoLongerAvailableException.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ConsumerArticleIsNoLongerAvailableException.cs @@ -9,7 +9,7 @@ internal sealed class ConsumerArticleIsNoLongerAvailableException : JsonApiExcep public string SupportEmailAddress { get; } public ConsumerArticleIsNoLongerAvailableException(string articleCode, string supportEmailAddress) - : base(new Error(HttpStatusCode.Gone) + : base(new ErrorObject(HttpStatusCode.Gone) { Title = "The requested article is no longer available.", Detail = $"Article with code '{articleCode}' is no longer available." diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs index adbb207632..ff8a61471f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -9,7 +10,6 @@ using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; using TestBuildingBlocks; using Xunit; @@ -55,7 +55,7 @@ public async Task Logs_and_produces_error_response_for_custom_exception() var consumerArticle = new ConsumerArticle { - Code = ConsumerArticleService.UnavailableArticlePrefix + "123" + Code = $"{ConsumerArticleService.UnavailableArticlePrefix}123" }; await _testContext.RunOnDatabaseAsync(async dbContext => @@ -64,21 +64,21 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/consumerArticles/" + consumerArticle.StringId; + string route = $"/consumerArticles/{consumerArticle.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Gone); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Gone); error.Title.Should().Be("The requested article is no longer available."); error.Detail.Should().Be("Article with code 'X123' is no longer available."); - error.Meta.Data["support"].Should().Be("Please contact us for info about similar articles at company@email.com."); + ((JsonElement)error.Meta["support"]).GetString().Should().Be("Please contact us for info about similar articles at company@email.com."); loggerFactory.Logger.Messages.Should().HaveCount(1); loggerFactory.Logger.Messages.Single().LogLevel.Should().Be(LogLevel.Warning); @@ -100,22 +100,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/throwingArticles/" + throwingArticle.StringId; + string route = $"/throwingArticles/{throwingArticle.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); error.Title.Should().Be("An unhandled error occurred while processing this request."); error.Detail.Should().Be("Exception has been thrown by the target of an invocation."); - IEnumerable stackTraceLines = ((JArray)error.Meta.Data["stackTrace"]).Select(token => token.Value()); + IEnumerable stackTraceLines = ((JsonElement)error.Meta["stackTrace"]).EnumerateArray().Select(token => token.GetString()); stackTraceLines.Should().ContainMatch("* System.InvalidOperationException: Article status could not be determined.*"); loggerFactory.Logger.Messages.Should().HaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingTests.cs index 517f9b2f1b..02073c4bdf 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingTests.cs @@ -46,27 +46,27 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be(HostPrefix + route); - responseDocument.Links.Last.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Last.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string galleryLink = HostPrefix + $"/iis-application-virtual-directory/public-api/artGalleries/{gallery.StringId}"; + string galleryLink = $"{HostPrefix}/iis-application-virtual-directory/public-api/artGalleries/{gallery.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(galleryLink); - responseDocument.ManyData[0].Relationships["paintings"].Links.Self.Should().Be(galleryLink + "/relationships/paintings"); - responseDocument.ManyData[0].Relationships["paintings"].Links.Related.Should().Be(galleryLink + "/paintings"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(galleryLink); + responseDocument.Data.ManyValue[0].Relationships["paintings"].Links.Self.Should().Be($"{galleryLink}/relationships/paintings"); + responseDocument.Data.ManyValue[0].Relationships["paintings"].Links.Related.Should().Be($"{galleryLink}/paintings"); - string paintingLink = HostPrefix + - $"/iis-application-virtual-directory/custom/path/to/paintings-of-the-world/{gallery.Paintings.ElementAt(0).StringId}"; + string paintingLink = + $"{HostPrefix}/iis-application-virtual-directory/custom/path/to/paintings-of-the-world/{gallery.Paintings.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(paintingLink); - responseDocument.Included[0].Relationships["exposedAt"].Links.Self.Should().Be(paintingLink + "/relationships/exposedAt"); - responseDocument.Included[0].Relationships["exposedAt"].Links.Related.Should().Be(paintingLink + "/exposedAt"); + responseDocument.Included[0].Relationships["exposedAt"].Links.Self.Should().Be($"{paintingLink}/relationships/exposedAt"); + responseDocument.Included[0].Relationships["exposedAt"].Links.Related.Should().Be($"{paintingLink}/exposedAt"); } [Fact] @@ -91,26 +91,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be(HostPrefix + route); - responseDocument.Links.Last.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Last.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string paintingLink = HostPrefix + $"/iis-application-virtual-directory/custom/path/to/paintings-of-the-world/{painting.StringId}"; + string paintingLink = $"{HostPrefix}/iis-application-virtual-directory/custom/path/to/paintings-of-the-world/{painting.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(paintingLink); - responseDocument.ManyData[0].Relationships["exposedAt"].Links.Self.Should().Be(paintingLink + "/relationships/exposedAt"); - responseDocument.ManyData[0].Relationships["exposedAt"].Links.Related.Should().Be(paintingLink + "/exposedAt"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(paintingLink); + responseDocument.Data.ManyValue[0].Relationships["exposedAt"].Links.Self.Should().Be($"{paintingLink}/relationships/exposedAt"); + responseDocument.Data.ManyValue[0].Relationships["exposedAt"].Links.Related.Should().Be($"{paintingLink}/exposedAt"); - string galleryLink = HostPrefix + $"/iis-application-virtual-directory/public-api/artGalleries/{painting.ExposedAt.StringId}"; + string galleryLink = $"{HostPrefix}/iis-application-virtual-directory/public-api/artGalleries/{painting.ExposedAt.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(galleryLink); - responseDocument.Included[0].Relationships["paintings"].Links.Self.Should().Be(galleryLink + "/relationships/paintings"); - responseDocument.Included[0].Relationships["paintings"].Links.Related.Should().Be(galleryLink + "/paintings"); + responseDocument.Included[0].Relationships["paintings"].Links.Self.Should().Be($"{galleryLink}/relationships/paintings"); + responseDocument.Included[0].Relationships["paintings"].Links.Related.Should().Be($"{galleryLink}/paintings"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs index 7d4f85b222..4d7c5b64ea 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs @@ -19,7 +19,7 @@ public int Decode(string value) if (!value.StartsWith("x", StringComparison.Ordinal)) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Invalid ID value.", Detail = $"The value '{value}' is not a valid hexadecimal value." @@ -53,7 +53,7 @@ public string Encode(int value) } string stringValue = value.ToString(); - return 'x' + ToHexString(stringValue); + return $"x{ToHexString(stringValue)}"; } private static string ToHexString(string value) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs index 73f8da6a5f..8c81bee08b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs @@ -44,8 +44,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(accounts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[1].StringId); } [Fact] @@ -62,7 +62,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); var codec = new HexadecimalCodec(); - string route = $"/bankAccounts?filter=any(id,'{accounts[1].StringId}','{codec.Encode(99999999)}')"; + string route = $"/bankAccounts?filter=any(id,'{accounts[1].StringId}','{codec.Encode(Unknown.TypedId.Int32)}')"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -70,8 +70,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(accounts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[1].StringId); } [Fact] @@ -81,14 +81,14 @@ public async Task Cannot_get_primary_resource_for_invalid_ID() const string route = "/bankAccounts/not-a-hex-value"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Invalid ID value."); error.Detail.Should().Be("The value 'not-a-hex-value' is not a valid hexadecimal value."); @@ -106,7 +106,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/debitCards/" + card.StringId; + string route = $"/debitCards/{card.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -114,8 +114,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(card.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(card.StringId); } [Fact] @@ -139,9 +139,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(account.Cards[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(account.Cards[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(account.Cards[0].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(account.Cards[1].StringId); } [Fact] @@ -165,8 +165,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(account.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(account.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(account.Cards[0].StringId); @@ -195,8 +195,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(account.Cards[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(account.Cards[0].StringId); } [Fact] @@ -244,11 +244,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Attributes["ownerName"].Should().Be(newCard.OwnerName); - responseDocument.SingleData.Attributes["pinCode"].Should().Be(newCard.PinCode); + responseDocument.Data.SingleValue.Attributes["ownerName"].Should().Be(newCard.OwnerName); + responseDocument.Data.SingleValue.Attributes["pinCode"].Should().Be(newCard.PinCode); var codec = new HexadecimalCodec(); - int newCardId = codec.Decode(responseDocument.SingleData.Id); + int newCardId = codec.Decode(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -307,7 +307,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/bankAccounts/" + existingAccount.StringId; + string route = $"/bankAccounts/{existingAccount.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -430,7 +430,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/bankAccounts/" + existingAccount.StringId; + string route = $"/bankAccounts/{existingAccount.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -449,23 +449,23 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_delete_missing_resource() + public async Task Cannot_delete_unknown_resource() { // Arrange var codec = new HexadecimalCodec(); - string stringId = codec.Encode(99999999); + string stringId = codec.Encode(Unknown.TypedId.Int32); - string route = "/bankAccounts/" + stringId; + string route = $"/bankAccounts/{stringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'bankAccounts' with ID '{stringId}' does not exist."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs index f3441bf5af..5e819e5ce6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs @@ -42,14 +42,14 @@ public async Task Cannot_create_resource_with_omitted_required_attribute() const string route = "/systemDirectories"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The Name field is required."); @@ -76,14 +76,14 @@ public async Task Cannot_create_resource_with_null_for_required_attribute_value( const string route = "/systemDirectories"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The Name field is required."); @@ -110,14 +110,14 @@ public async Task Cannot_create_resource_with_invalid_attribute_value() const string route = "/systemDirectories"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The field Name must match the regular expression '^[\\w\\s]+$'."); @@ -149,9 +149,9 @@ public async Task Can_create_resource_with_valid_attribute_value() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be("Projects"); - responseDocument.SingleData.Attributes["isCaseSensitive"].Should().Be(true); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be("Projects"); + responseDocument.Data.SingleValue.Attributes["isCaseSensitive"].Should().Be(true); } [Fact] @@ -173,26 +173,26 @@ public async Task Cannot_create_resource_with_multiple_violations() const string route = "/systemDirectories"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(3); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Name field is required."); error1.Source.Pointer.Should().Be("/data/attributes/name"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field SizeInBytes must be between 0 and 9223372036854775807."); error2.Source.Pointer.Should().Be("/data/attributes/sizeInBytes"); - Error error3 = responseDocument.Errors[2]; + ErrorObject error3 = responseDocument.Errors[2]; error3.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error3.Title.Should().Be("Input validation failed."); error3.Detail.Should().Be("The IsCaseSensitive field is required."); @@ -282,9 +282,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be("Projects"); - responseDocument.SingleData.Attributes["isCaseSensitive"].Should().Be(true); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be("Projects"); + responseDocument.Data.SingleValue.Attributes["isCaseSensitive"].Should().Be(true); } [Fact] @@ -361,7 +361,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -401,17 +401,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The Name field is required."); @@ -447,17 +447,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The field Name must match the regular expression '^[\\w\\s]+$'."); @@ -485,7 +485,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "systemDirectories", - id = -1, + id = "-1", attributes = new { name = "Repositories" @@ -499,7 +499,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "systemDirectories", - id = -1 + id = "-1" } } } @@ -510,20 +510,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/systemDirectories/-1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The field Id must match the regular expression '^[0-9]+$'."); error1.Source.Pointer.Should().Be("/data/attributes/id"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field Id must match the regular expression '^[0-9]+$'."); @@ -559,7 +559,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -670,7 +670,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -729,7 +729,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -783,7 +783,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -830,7 +830,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId + "/relationships/parent"; + string route = $"/systemDirectories/{directory.StringId}/relationships/parent"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -886,7 +886,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId + "/relationships/files"; + string route = $"/systemDirectories/{directory.StringId}/relationships/files"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/NoModelStateValidationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/NoModelStateValidationTests.cs index 2be7e452db..bbf49d0b73 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/NoModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/NoModelStateValidationTests.cs @@ -32,7 +32,7 @@ public async Task Can_create_resource_with_invalid_attribute_value() attributes = new { name = "!@#$%^&*().-", - isCaseSensitive = "false" + isCaseSensitive = false } } }; @@ -45,8 +45,8 @@ public async Task Can_create_resource_with_invalid_attribute_value() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be("!@#$%^&*().-"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be("!@#$%^&*().-"); } [Fact] @@ -78,7 +78,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs index cbe777c945..912bd9c9ac 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs @@ -71,11 +71,11 @@ private static void AssertHasValidInitialStage(Workflow resource) { if (resource.Stage != WorkflowStage.Created) { - throw new JsonApiException(new Error(HttpStatusCode.UnprocessableEntity) + throw new JsonApiException(new ErrorObject(HttpStatusCode.UnprocessableEntity) { Title = "Invalid workflow stage.", Detail = $"Initial stage of workflow must be '{WorkflowStage.Created}'.", - Source = + Source = new ErrorSource { Pointer = "/data/attributes/stage" } @@ -88,11 +88,11 @@ private static void AssertCanTransitionToStage(WorkflowStage fromStage, Workflow { if (!CanTransitionToStage(fromStage, toStage)) { - throw new JsonApiException(new Error(HttpStatusCode.UnprocessableEntity) + throw new JsonApiException(new ErrorObject(HttpStatusCode.UnprocessableEntity) { Title = "Invalid workflow stage.", Detail = $"Cannot transition from '{fromStage}' to '{toStage}'.", - Source = + Source = new ErrorSource { Pointer = "/data/attributes/stage" } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowTests.cs index 2631aa1d9c..d3cc69efb6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowTests.cs @@ -50,7 +50,7 @@ public async Task Can_create_in_valid_stage() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); } [Fact] @@ -72,14 +72,14 @@ public async Task Cannot_create_in_invalid_stage() const string route = "/workflows"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Invalid workflow stage."); error.Detail.Should().Be("Initial stage of workflow must be 'Created'."); @@ -114,17 +114,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workflows/" + existingWorkflow.StringId; + string route = $"/workflows/{existingWorkflow.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Invalid workflow stage."); error.Detail.Should().Be("Cannot transition from 'OnHold' to 'Succeeded'."); @@ -159,7 +159,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workflows/" + existingWorkflow.StringId; + string route = $"/workflows/{existingWorkflow.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs index 299a2a8f8d..7f03363f30 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs @@ -49,7 +49,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/api/photoAlbums/" + album.StringId; + string route = $"/api/photoAlbums/{album.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -57,17 +57,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(HostPrefix + route); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(HostPrefix + route + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(HostPrefix + route + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{HostPrefix}{route}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{HostPrefix}{route}/photos"); } [Fact] @@ -92,26 +92,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be(HostPrefix + route); - responseDocument.Links.Last.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Last.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = HostPrefix + $"/api/photoAlbums/{album.StringId}"; + string albumLink = $"{HostPrefix}/api/photoAlbums/{album.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(albumLink); - responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(albumLink); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); - string photoLink = HostPrefix + $"/api/photos/{album.Photos.ElementAt(0).StringId}"; + string photoLink = $"{HostPrefix}/api/photos/{album.Photos.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -135,19 +135,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = HostPrefix + $"/api/photoAlbums/{photo.Album.StringId}"; + string albumLink = $"{HostPrefix}/api/photoAlbums/{photo.Album.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } [Fact] @@ -171,19 +171,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string photoLink = HostPrefix + $"/api/photos/{album.Photos.ElementAt(0).StringId}"; + string photoLink = $"{HostPrefix}/api/photos/{album.Photos.ElementAt(0).StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(photoLink); - responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(photoLink); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -207,16 +207,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.Related.Should().Be(HostPrefix + $"/api/photos/{photo.StringId}/album"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Related.Should().Be($"{HostPrefix}/api/photos/{photo.StringId}/album"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Should().BeNull(); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } [Fact] @@ -240,16 +240,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.Related.Should().Be(HostPrefix + $"/api/photoAlbums/{album.StringId}/photos"); - responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Related.Should().Be($"{HostPrefix}/api/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); } [Fact] @@ -294,25 +294,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = HostPrefix + $"/api/photoAlbums/{responseDocument.SingleData.Id}"; + string albumLink = $"{HostPrefix}/api/photoAlbums/{responseDocument.Data.SingleValue.Id}"; - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); - string photoLink = HostPrefix + $"/api/photos/{existingPhoto.StringId}"; + string photoLink = $"{HostPrefix}/api/photos/{existingPhoto.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -356,26 +356,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string photoLink = HostPrefix + $"/api/photos/{existingPhoto.StringId}"; + string photoLink = $"{HostPrefix}/api/photos/{existingPhoto.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(photoLink); - responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(photoLink); + responseDocument.Data.SingleValue.Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.SingleValue.Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); - string albumLink = HostPrefix + $"/api/photoAlbums/{existingAlbum.StringId}"; + string albumLink = $"{HostPrefix}/api/photoAlbums/{existingAlbum.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(albumLink); - responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs index 7d0e239c1a..26d5dcc46a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs @@ -49,7 +49,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/photoAlbums/" + album.StringId; + string route = $"/photoAlbums/{album.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -57,17 +57,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(HostPrefix + route); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(HostPrefix + route + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(HostPrefix + route + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{HostPrefix}{route}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{HostPrefix}{route}/photos"); } [Fact] @@ -92,26 +92,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be(HostPrefix + route); - responseDocument.Links.Last.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Last.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = HostPrefix + $"/photoAlbums/{album.StringId}"; + string albumLink = $"{HostPrefix}/photoAlbums/{album.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(albumLink); - responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(albumLink); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); - string photoLink = HostPrefix + $"/photos/{album.Photos.ElementAt(0).StringId}"; + string photoLink = $"{HostPrefix}/photos/{album.Photos.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -135,19 +135,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = HostPrefix + $"/photoAlbums/{photo.Album.StringId}"; + string albumLink = $"{HostPrefix}/photoAlbums/{photo.Album.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } [Fact] @@ -171,19 +171,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string photoLink = HostPrefix + $"/photos/{album.Photos.ElementAt(0).StringId}"; + string photoLink = $"{HostPrefix}/photos/{album.Photos.ElementAt(0).StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(photoLink); - responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(photoLink); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -207,16 +207,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.Related.Should().Be(HostPrefix + $"/photos/{photo.StringId}/album"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Related.Should().Be($"{HostPrefix}/photos/{photo.StringId}/album"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Should().BeNull(); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } [Fact] @@ -240,16 +240,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.Related.Should().Be(HostPrefix + $"/photoAlbums/{album.StringId}/photos"); - responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Related.Should().Be($"{HostPrefix}/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); } [Fact] @@ -294,25 +294,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = HostPrefix + $"/photoAlbums/{responseDocument.SingleData.Id}"; + string albumLink = $"{HostPrefix}/photoAlbums/{responseDocument.Data.SingleValue.Id}"; - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); - string photoLink = HostPrefix + $"/photos/{existingPhoto.StringId}"; + string photoLink = $"{HostPrefix}/photos/{existingPhoto.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -356,26 +356,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string photoLink = HostPrefix + $"/photos/{existingPhoto.StringId}"; + string photoLink = $"{HostPrefix}/photos/{existingPhoto.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(photoLink); - responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(photoLink); + responseDocument.Data.SingleValue.Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.SingleValue.Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); - string albumLink = HostPrefix + $"/photoAlbums/{existingAlbum.StringId}"; + string albumLink = $"{HostPrefix}/photoAlbums/{existingAlbum.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(albumLink); - responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionTests.cs index d000d18499..e7aee555c3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionTests.cs @@ -45,11 +45,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Should().BeNull(); - responseDocument.SingleData.Relationships["photo"].Links.Self.Should().BeNull(); - responseDocument.SingleData.Relationships["photo"].Links.Related.Should().NotBeNull(); - responseDocument.SingleData.Relationships["album"].Links.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships["photo"].Links.Self.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships["photo"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["album"].Links.Should().BeNull(); responseDocument.Included.Should().HaveCount(2); @@ -85,11 +85,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Should().BeNull(); - responseDocument.SingleData.Relationships["photo"].Links.Self.Should().BeNull(); - responseDocument.SingleData.Relationships["photo"].Links.Related.Should().NotBeNull(); - responseDocument.SingleData.Relationships.Should().NotContainKey("album"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships["photo"].Links.Self.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships["photo"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships.Should().NotContainKey("album"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs index 4bc2e3f465..effa2b0d8c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs @@ -47,7 +47,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/api/photoAlbums/" + album.StringId; + string route = $"/api/photoAlbums/{album.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -62,10 +62,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(route); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(route + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(route + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(route); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{route}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{route}/photos"); } [Fact] @@ -99,17 +99,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string albumLink = $"/api/photoAlbums/{album.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(albumLink); - responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(albumLink); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); string photoLink = $"/api/photos/{album.Photos.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -142,10 +142,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string albumLink = $"/api/photoAlbums/{photo.Album.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } [Fact] @@ -178,10 +178,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string photoLink = $"/api/photos/{album.Photos.ElementAt(0).StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(photoLink); - responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(photoLink); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -212,9 +212,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Should().BeNull(); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } [Fact] @@ -245,9 +245,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); } [Fact] @@ -299,18 +299,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = $"/api/photoAlbums/{responseDocument.SingleData.Id}"; + string albumLink = $"/api/photoAlbums/{responseDocument.Data.SingleValue.Id}"; - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); string photoLink = $"/api/photos/{existingPhoto.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -363,17 +363,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string photoLink = $"/api/photos/{existingPhoto.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(photoLink); - responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(photoLink); + responseDocument.Data.SingleValue.Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.SingleValue.Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); string albumLink = $"/api/photoAlbums/{existingAlbum.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(albumLink); - responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs index 5221df597b..6256adf66d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs @@ -47,7 +47,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/photoAlbums/" + album.StringId; + string route = $"/photoAlbums/{album.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -62,10 +62,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(route); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(route + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(route + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(route); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{route}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{route}/photos"); } [Fact] @@ -99,17 +99,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string albumLink = $"/photoAlbums/{album.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(albumLink); - responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(albumLink); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); string photoLink = $"/photos/{album.Photos.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -142,10 +142,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string albumLink = $"/photoAlbums/{photo.Album.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } [Fact] @@ -178,10 +178,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string photoLink = $"/photos/{album.Photos.ElementAt(0).StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(photoLink); - responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(photoLink); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -212,9 +212,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Should().BeNull(); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } [Fact] @@ -245,9 +245,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); } [Fact] @@ -299,18 +299,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = $"/photoAlbums/{responseDocument.SingleData.Id}"; + string albumLink = $"/photoAlbums/{responseDocument.Data.SingleValue.Id}"; - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); string photoLink = $"/photos/{existingPhoto.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -363,17 +363,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string photoLink = $"/photos/{existingPhoto.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(photoLink); - responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(photoLink); + responseDocument.Data.SingleValue.Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.SingleValue.Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); string albumLink = $"/photoAlbums/{existingAlbum.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(albumLink); - responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResourceMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResourceMetaTests.cs index 3c6f386ed7..fbd9892f9a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResourceMetaTests.cs @@ -40,8 +40,8 @@ public async Task Returns_resource_meta_from_ResourceDefinition() var hitCounter = _testContext.Factory.Services.GetRequiredService(); List tickets = _fakers.SupportTicket.Generate(3); - tickets[0].Description = "Critical: " + tickets[0].Description; - tickets[2].Description = "Critical: " + tickets[2].Description; + tickets[0].Description = $"Critical: {tickets[0].Description}"; + tickets[2].Description = $"Critical: {tickets[2].Description}"; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -58,10 +58,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Meta.Should().ContainKey("hasHighPriority"); - responseDocument.ManyData[1].Meta.Should().BeNull(); - responseDocument.ManyData[2].Meta.Should().ContainKey("hasHighPriority"); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Meta.Should().ContainKey("hasHighPriority"); + responseDocument.Data.ManyValue[1].Meta.Should().BeNull(); + responseDocument.Data.ManyValue[2].Meta.Should().ContainKey("hasHighPriority"); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -79,7 +79,7 @@ public async Task Returns_resource_meta_from_ResourceDefinition_in_included_reso ProductFamily family = _fakers.ProductFamily.Generate(); family.Tickets = _fakers.SupportTicket.Generate(1); - family.Tickets[0].Description = "Critical: " + family.Tickets[0].Description; + family.Tickets[0].Description = $"Critical: {family.Tickets[0].Description}"; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -96,7 +96,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Meta.Should().ContainKey("hasHighPriority"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs index 15297d7b37..0c698644da 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs @@ -48,6 +48,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Should().BeJson(@"{ + ""links"": { + ""self"": ""http://localhost/supportTickets"", + ""first"": ""http://localhost/supportTickets"" + }, + ""data"": [], ""meta"": { ""license"": ""MIT"", ""projectUrl"": ""https://github.com/json-api-dotnet/JsonApiDotNetCore/"", @@ -57,12 +62,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""v2.5.2"", ""v1.3.1"" ] - }, - ""links"": { - ""self"": ""http://localhost/supportTickets"", - ""first"": ""http://localhost/supportTickets"" - }, - ""data"": [] + } }"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/TopLevelCountTests.cs index 7041d5b6e5..a449e1799b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/TopLevelCountTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/TopLevelCountTests.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -54,7 +55,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.Should().NotBeNull(); - responseDocument.Meta["totalResources"].Should().Be(1); + ((JsonElement)responseDocument.Meta["total"]).GetInt32().Should().Be(1); } [Fact] @@ -75,7 +76,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.Should().NotBeNull(); - responseDocument.Meta["totalResources"].Should().Be(0); + ((JsonElement)responseDocument.Meta["total"]).GetInt32().Should().Be(0); } [Fact] @@ -134,7 +135,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/supportTickets/" + existingTicket.StringId; + string route = $"/supportTickets/{existingTicket.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs index be56f0e5fa..bf762c857b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs @@ -43,8 +43,8 @@ public async Task Create_group_sends_messages() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroupName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(newGroupName); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -55,7 +55,7 @@ public async Task Create_group_sends_messages() messageBroker.SentMessages.Should().HaveCount(1); - Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + Guid newGroupId = Guid.Parse(responseDocument.Data.SingleValue.Id); var content = messageBroker.SentMessages[0].GetContentAs(); content.GroupId.Should().Be(newGroupId); @@ -121,8 +121,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroupName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(newGroupName); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -134,7 +134,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => messageBroker.SentMessages.Should().HaveCount(3); - Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + Guid newGroupId = Guid.Parse(responseDocument.Data.SingleValue.Id); var content1 = messageBroker.SentMessages[0].GetContentAs(); content1.GroupId.Should().Be(newGroupId); @@ -180,7 +180,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -264,7 +264,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -313,7 +313,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -351,7 +351,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs index fa591caa2b..fe40df6188 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs @@ -44,9 +44,9 @@ public async Task Create_user_sends_messages() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["loginName"].Should().Be(newLoginName); - responseDocument.SingleData.Attributes["displayName"].Should().Be(newDisplayName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["loginName"].Should().Be(newLoginName); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().Be(newDisplayName); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -57,7 +57,7 @@ public async Task Create_user_sends_messages() messageBroker.SentMessages.Should().HaveCount(1); - Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); + Guid newUserId = Guid.Parse(responseDocument.Data.SingleValue.Id); var content = messageBroker.SentMessages[0].GetContentAs(); content.UserId.Should().Be(newUserId); @@ -113,9 +113,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["loginName"].Should().Be(newLoginName); - responseDocument.SingleData.Attributes["displayName"].Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["loginName"].Should().Be(newLoginName); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().BeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -127,7 +127,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => messageBroker.SentMessages.Should().HaveCount(2); - Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); + Guid newUserId = Guid.Parse(responseDocument.Data.SingleValue.Id); var content1 = messageBroker.SentMessages[0].GetContentAs(); content1.UserId.Should().Be(newUserId); @@ -171,7 +171,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -239,7 +239,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -311,7 +311,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -385,7 +385,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -431,7 +431,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -469,7 +469,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs index a8a2c5a373..391ff96781 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs @@ -46,22 +46,22 @@ public async Task Does_not_send_message_on_write_error() var hitCounter = _testContext.Factory.Services.GetRequiredService(); var messageBroker = _testContext.Factory.Services.GetRequiredService(); - string missingUserId = Guid.NewGuid().ToString(); + string unknownUserId = Unknown.StringId.For(); - string route = "/domainUsers/" + missingUserId; + string route = $"/domainUsers/{unknownUserId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be($"Resource of type 'domainUsers' with ID '{missingUserId}' does not exist."); + error.Detail.Should().Be($"Resource of type 'domainUsers' with ID '{unknownUserId}' does not exist."); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -89,17 +89,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.ServiceUnavailable); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.ServiceUnavailable); error.Title.Should().Be("Message delivery failed."); error.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/MessageBroker.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/MessageBroker.cs index ee8b2fae7f..1fbf64df46 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/MessageBroker.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/MessageBroker.cs @@ -27,7 +27,7 @@ internal Task PostMessageAsync(OutgoingMessage message, CancellationToken cancel if (SimulateFailure) { - throw new JsonApiException(new Error(HttpStatusCode.ServiceUnavailable) + throw new JsonApiException(new ErrorObject(HttpStatusCode.ServiceUnavailable) { Title = "Message delivery failed." }); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs index f1ed19ef3e..fccf23a8ef 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs @@ -1,5 +1,5 @@ +using System.Text.Json; using JetBrains.Annotations; -using Newtonsoft.Json; namespace JsonApiDotNetCoreTests.IntegrationTests.Microservices.Messages { @@ -15,9 +15,9 @@ public T GetContentAs() where T : IMessageContent { string namespacePrefix = typeof(IMessageContent).Namespace; - var contentType = System.Type.GetType(namespacePrefix + "." + Type, true); + var contentType = System.Type.GetType($"{namespacePrefix}.{Type}", true); - return (T)JsonConvert.DeserializeObject(Content, contentType); + return (T)JsonSerializer.Deserialize(Content, contentType); } public static OutgoingMessage CreateFromContent(IMessageContent content) @@ -26,7 +26,7 @@ public static OutgoingMessage CreateFromContent(IMessageContent content) { Type = content.GetType().Name, FormatVersion = content.FormatVersion, - Content = JsonConvert.SerializeObject(content) + Content = JsonSerializer.Serialize(content, content.GetType()) }; } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs index 5867e2d962..eda25094b2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs @@ -49,8 +49,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroupName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(newGroupName); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -58,7 +58,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) }, options => options.WithStrictOrdering()); - Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + Guid newGroupId = Guid.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -130,8 +130,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroupName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(newGroupName); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -140,7 +140,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) }, options => options.WithStrictOrdering()); - Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + Guid newGroupId = Guid.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -192,7 +192,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -279,7 +279,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -331,7 +331,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -372,7 +372,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs index 2a5d518b66..219da42053 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs @@ -51,9 +51,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["loginName"].Should().Be(newLoginName); - responseDocument.SingleData.Attributes["displayName"].Should().Be(newDisplayName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["loginName"].Should().Be(newLoginName); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().Be(newDisplayName); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -61,7 +61,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) }, options => options.WithStrictOrdering()); - Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); + Guid newUserId = Guid.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -123,9 +123,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["loginName"].Should().Be(newLoginName); - responseDocument.SingleData.Attributes["displayName"].Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["loginName"].Should().Be(newLoginName); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().BeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -134,7 +134,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) }, options => options.WithStrictOrdering()); - Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); + Guid newUserId = Guid.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -184,7 +184,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -255,7 +255,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -330,7 +330,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -407,7 +407,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -456,7 +456,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -497,7 +497,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs index 8bfb695d55..68af373b9d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs @@ -51,7 +51,7 @@ public async Task Does_not_add_to_outbox_on_write_error() DomainUser existingUser = _fakers.DomainUser.Generate(); - string missingUserId = Guid.NewGuid().ToString(); + string unknownUserId = Unknown.StringId.For(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -72,7 +72,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "domainUsers", - id = missingUserId + id = unknownUserId } } }; @@ -80,17 +80,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/domainGroups/{existingGroup.StringId}/relationships/users"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be($"Related resource of type 'domainUsers' with ID '{missingUserId}' in relationship 'users' does not exist."); + error.Detail.Should().Be($"Related resource of type 'domainUsers' with ID '{unknownUserId}' in relationship 'users' does not exist."); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs index 9faebf948c..11c4af8983 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs @@ -68,8 +68,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(shops[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(shops[1].StringId); } [Fact] @@ -98,8 +98,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(shops[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(shops[1].StringId); } [Fact] @@ -128,9 +128,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Type.Should().Be("webShops"); - responseDocument.ManyData[0].Id.Should().Be(shops[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Type.Should().Be("webShops"); + responseDocument.Data.ManyValue[0].Id.Should().Be(shops[1].StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("webProducts"); @@ -150,17 +150,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/nld/shops/" + shop.StringId; + string route = $"/nld/shops/{shop.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webShops' with ID '{shop.StringId}' does not exist."); @@ -183,14 +183,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{shop.StringId}/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webShops' with ID '{shop.StringId}' does not exist."); @@ -213,14 +213,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/products/{product.StringId}/shop"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webProducts' with ID '{product.StringId}' does not exist."); @@ -243,14 +243,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{shop.StringId}/relationships/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webShops' with ID '{shop.StringId}' does not exist."); @@ -273,14 +273,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/products/{product.StringId}/relationships/shop"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webProducts' with ID '{product.StringId}' does not exist."); @@ -312,11 +312,11 @@ public async Task Can_create_resource() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["url"].Should().Be(newShopUrl); - responseDocument.SingleData.Relationships.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["url"].Should().Be(newShopUrl); + responseDocument.Data.SingleValue.Relationships.Should().NotBeNull(); - int newShopId = int.Parse(responseDocument.SingleData.Id); + int newShopId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -372,14 +372,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/nld/shops"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webProducts' with ID '{existingProduct.StringId}' in relationship 'products' does not exist."); @@ -426,14 +426,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/nld/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webShops' with ID '{existingShop.StringId}' in relationship 'shop' does not exist."); @@ -468,7 +468,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/nld/products/" + existingProduct.StringId; + string route = $"/nld/products/{existingProduct.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -516,17 +516,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/nld/products/" + existingProduct.StringId; + string route = $"/nld/products/{existingProduct.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webProducts' with ID '{existingProduct.StringId}' does not exist."); @@ -572,17 +572,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/nld/shops/" + existingShop.StringId; + string route = $"/nld/shops/{existingShop.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webProducts' with ID '{existingProduct.StringId}' in relationship 'products' does not exist."); @@ -625,17 +625,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/nld/products/" + existingProduct.StringId; + string route = $"/nld/products/{existingProduct.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webShops' with ID '{existingShop.StringId}' in relationship 'shop' does not exist."); @@ -663,14 +663,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{existingShop.StringId}/relationships/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webShops' with ID '{existingShop.StringId}' does not exist."); @@ -708,14 +708,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{existingShop.StringId}/relationships/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webProducts' with ID '{existingProduct.StringId}' in relationship 'products' does not exist."); @@ -743,14 +743,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/products/{existingProduct.StringId}/relationships/shop"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webProducts' with ID '{existingProduct.StringId}' does not exist."); @@ -785,14 +785,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/products/{existingProduct.StringId}/relationships/shop"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webShops' with ID '{existingShop.StringId}' in relationship 'shop' does not exist."); @@ -830,14 +830,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{existingShop.StringId}/relationships/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webShops' with ID '{existingShop.StringId}' does not exist."); @@ -874,14 +874,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{existingShop.StringId}/relationships/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webProducts' with ID '{existingProduct.StringId}' in relationship 'products' does not exist."); @@ -916,14 +916,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{existingShop.StringId}/relationships/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webShops' with ID '{existingShop.StringId}' does not exist."); @@ -943,7 +943,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/nld/products/" + existingProduct.StringId; + string route = $"/nld/products/{existingProduct.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -975,17 +975,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/nld/products/" + existingProduct.StringId; + string route = $"/nld/products/{existingProduct.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webProducts' with ID '{existingProduct.StringId}' does not exist."); @@ -1023,17 +1023,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string shopLink = $"/nld/shops/{shop.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(shopLink); - responseDocument.ManyData[0].Relationships["products"].Links.Self.Should().Be(shopLink + "/relationships/products"); - responseDocument.ManyData[0].Relationships["products"].Links.Related.Should().Be(shopLink + "/products"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(shopLink); + responseDocument.Data.ManyValue[0].Relationships["products"].Links.Self.Should().Be($"{shopLink}/relationships/products"); + responseDocument.Data.ManyValue[0].Relationships["products"].Links.Related.Should().Be($"{shopLink}/products"); string productLink = $"/nld/products/{shop.Products[0].StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(productLink); - responseDocument.Included[0].Relationships["shop"].Links.Self.Should().Be(productLink + "/relationships/shop"); - responseDocument.Included[0].Relationships["shop"].Links.Related.Should().Be(productLink + "/shop"); + responseDocument.Included[0].Relationships["shop"].Links.Self.Should().Be($"{productLink}/relationships/shop"); + responseDocument.Included[0].Relationships["shop"].Links.Related.Should().Be($"{productLink}/shop"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/JsonKebabCaseNamingPolicy.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/JsonKebabCaseNamingPolicy.cs new file mode 100644 index 0000000000..1c50a67fb8 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/JsonKebabCaseNamingPolicy.cs @@ -0,0 +1,83 @@ +using System; +using System.Text; +using System.Text.Json; + +namespace JsonApiDotNetCoreTests.IntegrationTests.NamingConventions +{ + // Based on https://github.com/J0rgeSerran0/JsonNamingPolicy + internal sealed class JsonKebabCaseNamingPolicy : JsonNamingPolicy + { + private const char Separator = '-'; + + public static readonly JsonKebabCaseNamingPolicy Instance = new(); + + public override string ConvertName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return string.Empty; + } + + ReadOnlySpan spanName = name.Trim(); + + var stringBuilder = new StringBuilder(); + bool addCharacter = true; + + bool isNextLower = false; + bool isNextUpper = false; + bool isNextSpace = false; + + for (int position = 0; position < spanName.Length; position++) + { + if (position != 0) + { + bool isCurrentSpace = spanName[position] == 32; + bool isPreviousSpace = spanName[position - 1] == 32; + bool isPreviousSeparator = spanName[position - 1] == 95; + + if (position + 1 != spanName.Length) + { + isNextLower = spanName[position + 1] > 96 && spanName[position + 1] < 123; + isNextUpper = spanName[position + 1] > 64 && spanName[position + 1] < 91; + isNextSpace = spanName[position + 1] == 32; + } + + if (isCurrentSpace && (isPreviousSpace || isPreviousSeparator || isNextUpper || isNextSpace)) + { + addCharacter = false; + } + else + { + bool isCurrentUpper = spanName[position] > 64 && spanName[position] < 91; + bool isPreviousLower = spanName[position - 1] > 96 && spanName[position - 1] < 123; + bool isPreviousNumber = spanName[position - 1] > 47 && spanName[position - 1] < 58; + + if (isCurrentUpper && (isPreviousLower || isPreviousNumber || isNextLower || isNextSpace)) + { + stringBuilder.Append(Separator); + } + else + { + if (isCurrentSpace) + { + stringBuilder.Append(Separator); + addCharacter = false; + } + } + } + } + + if (addCharacter) + { + stringBuilder.Append(spanName[position]); + } + else + { + addCharacter = true; + } + } + + return stringBuilder.ToString().ToLower(); + } + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs index fb5241d17e..952ffd7a14 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs @@ -1,7 +1,6 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json.Serialization; using TestBuildingBlocks; namespace JsonApiDotNetCoreTests.IntegrationTests.NamingConventions @@ -19,10 +18,8 @@ protected override void SetJsonApiOptions(JsonApiOptions options) options.IncludeTotalResourceCount = true; options.ValidateModelState = true; - options.SerializerSettings.ContractResolver = new DefaultContractResolver - { - NamingStrategy = new KebabCaseNamingStrategy() - }; + options.SerializerOptions.PropertyNamingPolicy = JsonKebabCaseNamingPolicy.Instance; + options.SerializerOptions.DictionaryKeyPolicy = JsonKebabCaseNamingPolicy.Instance; } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs index e960c607a5..de91ad49fb 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -44,11 +45,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Type == "swimming-pools"); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Attributes.ContainsKey("is-indoor")); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Relationships.ContainsKey("water-slides")); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Relationships.ContainsKey("diving-boards")); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Type == "swimming-pools"); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Attributes.ContainsKey("is-indoor")); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Relationships.ContainsKey("water-slides")); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Relationships.ContainsKey("diving-boards")); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("diving-boards"); @@ -57,7 +58,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Relationships.Should().BeNull(); responseDocument.Included[0].Links.Self.Should().Be($"/public-api/diving-boards/{pools[1].DivingBoards[0].StringId}"); - responseDocument.Meta["total-resources"].Should().Be(2); + ((JsonElement)responseDocument.Meta["total"]).GetInt32().Should().Be(2); } [Fact] @@ -84,10 +85,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Type.Should().Be("water-slides"); - responseDocument.ManyData[0].Id.Should().Be(pool.WaterSlides[1].StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Type.Should().Be("water-slides"); + responseDocument.Data.ManyValue[0].Id.Should().Be(pool.WaterSlides[1].StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); } [Fact] @@ -116,18 +117,18 @@ public async Task Can_create_resource() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("swimming-pools"); - responseDocument.SingleData.Attributes["is-indoor"].Should().Be(newPool.IsIndoor); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("swimming-pools"); + responseDocument.Data.SingleValue.Attributes["is-indoor"].Should().Be(newPool.IsIndoor); - int newPoolId = int.Parse(responseDocument.SingleData.Id); - string poolLink = route + $"/{newPoolId}"; + int newPoolId = int.Parse(responseDocument.Data.SingleValue.Id); + string poolLink = $"{route}/{newPoolId}"; - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships["water-slides"].Links.Self.Should().Be(poolLink + "/relationships/water-slides"); - responseDocument.SingleData.Relationships["water-slides"].Links.Related.Should().Be(poolLink + "/water-slides"); - responseDocument.SingleData.Relationships["diving-boards"].Links.Self.Should().Be(poolLink + "/relationships/diving-boards"); - responseDocument.SingleData.Relationships["diving-boards"].Links.Related.Should().Be(poolLink + "/diving-boards"); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships["water-slides"].Links.Self.Should().Be($"{poolLink}/relationships/water-slides"); + responseDocument.Data.SingleValue.Relationships["water-slides"].Links.Related.Should().Be($"{poolLink}/water-slides"); + responseDocument.Data.SingleValue.Relationships["diving-boards"].Links.Self.Should().Be($"{poolLink}/relationships/diving-boards"); + responseDocument.Data.SingleValue.Relationships["diving-boards"].Links.Related.Should().Be($"{poolLink}/diving-boards"); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -146,17 +147,17 @@ public async Task Applies_casing_convention_on_error_stack_trace() const string route = "/public-api/swimming-pools"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Meta.Data.Should().ContainKey("stack-trace"); + error.Meta.Should().ContainKey("stack-trace"); } [Fact] @@ -184,17 +185,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/public-api/diving-boards/" + existingBoard.StringId; + string route = $"/public-api/diving-boards/{existingBoard.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The field HeightInMeters must be between 1 and 20."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingConventionStartup.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingConventionStartup.cs new file mode 100644 index 0000000000..65deafe3bc --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingConventionStartup.cs @@ -0,0 +1,25 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using Microsoft.EntityFrameworkCore; +using TestBuildingBlocks; + +namespace JsonApiDotNetCoreTests.IntegrationTests.NamingConventions +{ + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class PascalCasingConventionStartup : TestableStartup + where TDbContext : DbContext + { + protected override void SetJsonApiOptions(JsonApiOptions options) + { + base.SetJsonApiOptions(options); + + options.Namespace = "PublicApi"; + options.UseRelativeLinks = true; + options.IncludeTotalResourceCount = true; + options.ValidateModelState = true; + + options.SerializerOptions.PropertyNamingPolicy = null; + options.SerializerOptions.DictionaryKeyPolicy = null; + } + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs new file mode 100644 index 0000000000..d1c511bda9 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs @@ -0,0 +1,205 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using TestBuildingBlocks; +using Xunit; + +namespace JsonApiDotNetCoreTests.IntegrationTests.NamingConventions +{ + public sealed class PascalCasingTests : IClassFixture, SwimmingDbContext>> + { + private readonly IntegrationTestContext, SwimmingDbContext> _testContext; + private readonly SwimmingFakers _fakers = new(); + + public PascalCasingTests(IntegrationTestContext, SwimmingDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.UseController(); + } + + [Fact] + public async Task Can_get_resources_with_include() + { + // Arrange + List pools = _fakers.SwimmingPool.Generate(2); + pools[1].DivingBoards = _fakers.DivingBoard.Generate(1); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.SwimmingPools.AddRange(pools); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/PublicApi/SwimmingPools?include=DivingBoards"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Type == "SwimmingPools"); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Attributes.ContainsKey("IsIndoor")); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Relationships.ContainsKey("WaterSlides")); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Relationships.ContainsKey("DivingBoards")); + + responseDocument.Included.Should().HaveCount(1); + responseDocument.Included[0].Type.Should().Be("DivingBoards"); + responseDocument.Included[0].Id.Should().Be(pools[1].DivingBoards[0].StringId); + responseDocument.Included[0].Attributes["HeightInMeters"].As().Should().BeApproximately(pools[1].DivingBoards[0].HeightInMeters); + responseDocument.Included[0].Relationships.Should().BeNull(); + responseDocument.Included[0].Links.Self.Should().Be($"/PublicApi/DivingBoards/{pools[1].DivingBoards[0].StringId}"); + + ((JsonElement)responseDocument.Meta["Total"]).GetInt32().Should().Be(2); + } + + [Fact] + public async Task Can_filter_secondary_resources_with_sparse_fieldset() + { + // Arrange + SwimmingPool pool = _fakers.SwimmingPool.Generate(); + pool.WaterSlides = _fakers.WaterSlide.Generate(2); + pool.WaterSlides[0].LengthInMeters = 1; + pool.WaterSlides[1].LengthInMeters = 5; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.SwimmingPools.Add(pool); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/PublicApi/SwimmingPools/{pool.StringId}/WaterSlides" + + "?filter=greaterThan(LengthInMeters,'1')&fields[WaterSlides]=LengthInMeters"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Type.Should().Be("WaterSlides"); + responseDocument.Data.ManyValue[0].Id.Should().Be(pool.WaterSlides[1].StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + } + + [Fact] + public async Task Can_create_resource() + { + // Arrange + SwimmingPool newPool = _fakers.SwimmingPool.Generate(); + + var requestBody = new + { + data = new + { + type = "SwimmingPools", + attributes = new Dictionary + { + ["IsIndoor"] = newPool.IsIndoor + } + } + }; + + const string route = "/PublicApi/SwimmingPools"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("SwimmingPools"); + responseDocument.Data.SingleValue.Attributes["IsIndoor"].Should().Be(newPool.IsIndoor); + + int newPoolId = int.Parse(responseDocument.Data.SingleValue.Id); + string poolLink = $"{route}/{newPoolId}"; + + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships["WaterSlides"].Links.Self.Should().Be($"{poolLink}/relationships/WaterSlides"); + responseDocument.Data.SingleValue.Relationships["WaterSlides"].Links.Related.Should().Be($"{poolLink}/WaterSlides"); + responseDocument.Data.SingleValue.Relationships["DivingBoards"].Links.Self.Should().Be($"{poolLink}/relationships/DivingBoards"); + responseDocument.Data.SingleValue.Relationships["DivingBoards"].Links.Related.Should().Be($"{poolLink}/DivingBoards"); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + SwimmingPool poolInDatabase = await dbContext.SwimmingPools.FirstWithIdAsync(newPoolId); + + poolInDatabase.IsIndoor.Should().Be(newPool.IsIndoor); + }); + } + + [Fact] + public async Task Applies_casing_convention_on_error_stack_trace() + { + // Arrange + const string requestBody = "{ \"data\": {"; + + const string route = "/PublicApi/SwimmingPools"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + + ErrorObject error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Meta.Should().ContainKey("StackTrace"); + } + + [Fact] + public async Task Applies_casing_convention_on_source_pointer_from_ModelState() + { + // Arrange + DivingBoard existingBoard = _fakers.DivingBoard.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.DivingBoards.Add(existingBoard); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "DivingBoards", + id = existingBoard.StringId, + attributes = new Dictionary + { + ["HeightInMeters"] = -1 + } + } + }; + + string route = $"/PublicApi/DivingBoards/{existingBoard.StringId}"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + + ErrorObject error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Input validation failed."); + error.Detail.Should().Be("The field HeightInMeters must be between 1 and 20."); + error.Source.Pointer.Should().Be("/data/attributes/HeightInMeters"); + } + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Calendar.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Calendar.cs index 5ca3125da3..90ffe9d9b3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Calendar.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Calendar.cs @@ -11,6 +11,9 @@ public sealed class Calendar : Identifiable [Attr] public string TimeZone { get; set; } + [Attr] + public bool ShowWeekNumbers { get; set; } + [Attr] public int DefaultAppointmentDurationInMinutes { get; set; } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs index df1677f380..f021dd27e7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs @@ -1,7 +1,9 @@ using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; +using System.Text.Json.Serialization; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Common; @@ -27,6 +29,12 @@ public FilterDataTypeTests(IntegrationTestContext(); options.EnableLegacyFilterNotation = false; + + if (!options.SerializerOptions.Converters.Any(converter => converter is JsonStringEnumMemberConverter)) + { + options.SerializerOptions.Converters.Add(new JsonStringEnumMemberConverter()); + options.SerializerOptions.Converters.Add(new JsonTimeSpanConverter()); + } } [Theory] @@ -64,8 +72,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes[attributeName].Should().Be(value is Enum ? value.ToString() : value); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes[attributeName].Should().Be(value); } [Fact] @@ -92,8 +100,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someDecimal"].Should().Be(resource.SomeDecimal); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someDecimal"].Should().Be(resource.SomeDecimal); } [Fact] @@ -120,8 +128,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someGuid"].Should().Be(resource.SomeGuid.ToString()); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someGuid"].Should().Be(resource.SomeGuid); } [Fact] @@ -148,8 +156,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someDateTime"].Should().BeCloseTo(resource.SomeDateTime); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someDateTime"].As().Should().BeCloseTo(resource.SomeDateTime); } [Fact] @@ -176,8 +184,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someDateTimeOffset"].Should().BeCloseTo(resource.SomeDateTimeOffset); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someDateTimeOffset"].As().Should().BeCloseTo(resource.SomeDateTimeOffset); } [Fact] @@ -204,8 +212,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someTimeSpan"].Should().Be(resource.SomeTimeSpan.ToString()); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someTimeSpan"].Should().Be(resource.SomeTimeSpan); } [Fact] @@ -227,18 +235,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/filterableResources?filter=equals(someInt32,'ABC')"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Query creation failed due to incompatible types."); error.Detail.Should().Be("Failed to convert 'ABC' of type 'String' to type 'Int32'."); - error.Source.Parameter.Should().BeNull(); + error.Source.Should().BeNull(); } [Theory] @@ -291,8 +299,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes[attributeName].Should().Be(null); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes[attributeName].Should().Be(null); } [Theory] @@ -341,8 +349,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes[attributeName].Should().NotBe(null); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes[attributeName].Should().NotBe(null); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs index db03f0517e..4d13fd69c1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs @@ -55,8 +55,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); } [Fact] @@ -74,14 +74,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/blogPosts/{post.StringId}?filter=equals(caption,'Two')"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified filter is invalid."); error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); @@ -111,8 +111,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[1].StringId); } [Fact] @@ -130,14 +130,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/blogPosts/{post.StringId}/author?filter=equals(displayName,'John Smith')"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified filter is invalid."); error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); @@ -169,9 +169,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData.Should().ContainSingle(post => post.Id == posts[1].StringId); - responseDocument.ManyData.Should().ContainSingle(post => post.Id == posts[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().ContainSingle(post => post.Id == posts[1].StringId); + responseDocument.Data.ManyValue.Should().ContainSingle(post => post.Id == posts[2].StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(posts[1].Author.StringId); @@ -199,8 +199,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); } [Fact] @@ -228,8 +228,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); } [Fact] @@ -254,8 +254,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); } [Fact] @@ -285,8 +285,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); } [Fact] @@ -313,7 +313,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(blog.Posts[1].StringId); @@ -343,7 +343,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(blog.Owner.Posts[1].StringId); @@ -381,7 +381,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(posts[1].Labels.First().StringId); @@ -412,7 +412,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(blog.Owner.StringId); @@ -443,9 +443,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(posts[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(posts[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[0].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(posts[2].StringId); } [Fact] @@ -484,9 +484,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(posts[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[0].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(posts[1].StringId); } [Fact] @@ -526,8 +526,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); responseDocument.Included.Should().HaveCount(3); responseDocument.Included[0].Id.Should().Be(blogs[1].Owner.StringId); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs index 31b6aa41cd..842a26b5f0 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs @@ -53,8 +53,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someString"].Should().Be(resource.SomeString); } [Fact] @@ -88,9 +88,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); - responseDocument.ManyData[0].Attributes["otherInt32"].Should().Be(resource.OtherInt32); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); + responseDocument.Data.ManyValue[0].Attributes["otherInt32"].Should().Be(resource.OtherInt32); } [Fact] @@ -124,9 +124,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32); - responseDocument.ManyData[0].Attributes["otherNullableInt32"].Should().Be(resource.OtherNullableInt32); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32); + responseDocument.Data.ManyValue[0].Attributes["otherNullableInt32"].Should().Be(resource.OtherNullableInt32); } [Fact] @@ -160,9 +160,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); - responseDocument.ManyData[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); + responseDocument.Data.ManyValue[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32); } [Fact] @@ -196,9 +196,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); - responseDocument.ManyData[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); + responseDocument.Data.ManyValue[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32); } [Fact] @@ -232,9 +232,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); - responseDocument.ManyData[0].Attributes["someUnsignedInt64"].Should().Be(resource.SomeUnsignedInt64); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); + responseDocument.Data.ManyValue[0].Attributes["someUnsignedInt64"].Should().Be(resource.SomeUnsignedInt64); } [Fact] @@ -244,18 +244,18 @@ public async Task Cannot_filter_equality_on_two_attributes_of_incompatible_types const string route = "/filterableResources?filter=equals(someDouble,someTimeSpan)"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Query creation failed due to incompatible types."); error.Detail.Should().Be("No coercion operator is defined between types 'System.TimeSpan' and 'System.Double'."); - error.Source.Parameter.Should().BeNull(); + error.Source.Should().BeNull(); } [Theory] @@ -295,8 +295,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); } [Theory] @@ -337,8 +337,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someDouble"].Should().Be(resource.SomeDouble); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someDouble"].Should().Be(resource.SomeDouble); } [Theory] @@ -380,8 +380,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someDateTime"].Should().BeCloseTo(resource.SomeDateTime); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someDateTime"].As().Should().BeCloseTo(resource.SomeDateTime); } [Theory] @@ -418,8 +418,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someString"].Should().Be(resource.SomeString); } [Theory] @@ -453,8 +453,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someString"].Should().Be(resource.SomeString); } [Fact] @@ -484,8 +484,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(resource.StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(resource.StringId); } [Fact] @@ -531,8 +531,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(resources[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(resources[1].StringId); } [Fact] @@ -563,8 +563,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(resource.StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(resource.StringId); } [Theory] @@ -604,8 +604,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(resource1.StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(resource1.StringId); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs index e4e5fdeec5..8aa640c530 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs @@ -30,42 +30,42 @@ public FilterTests(IntegrationTestContext, public async Task Cannot_filter_in_unknown_scope() { // Arrange - const string route = "/webAccounts?filter[doesNotExist]=equals(title,null)"; + string route = $"/webAccounts?filter[{Unknown.Relationship}]=equals(title,null)"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified filter is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); - error.Source.Parameter.Should().Be("filter[doesNotExist]"); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource 'webAccounts'."); + error.Source.Parameter.Should().Be($"filter[{Unknown.Relationship}]"); } [Fact] public async Task Cannot_filter_in_unknown_nested_scope() { // Arrange - const string route = "/webAccounts?filter[posts.doesNotExist]=equals(title,null)"; + string route = $"/webAccounts?filter[posts.{Unknown.Relationship}]=equals(title,null)"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified filter is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); - error.Source.Parameter.Should().Be("filter[posts.doesNotExist]"); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource 'blogPosts'."); + error.Source.Parameter.Should().Be($"filter[posts.{Unknown.Relationship}]"); } [Fact] @@ -75,14 +75,14 @@ public async Task Cannot_filter_on_attribute_with_blocked_capability() const string route = "/webAccounts?filter=equals(dateOfBirth,null)"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Filtering on the requested attribute is not allowed."); error.Detail.Should().Be("Filtering on attribute 'dateOfBirth' is not allowed."); @@ -110,9 +110,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(accounts[0].StringId); - responseDocument.ManyData[0].Attributes["userName"].Should().Be(accounts[0].UserName); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[0].StringId); + responseDocument.Data.ManyValue[0].Attributes["userName"].Should().Be(accounts[0].UserName); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs index 39bd0e68b5..7236e285eb 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -52,9 +53,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.StringId); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); + responseDocument.Data.ManyValue[0].Attributes["caption"].Should().Be(post.Caption); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("webAccounts"); @@ -83,9 +84,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("webAccounts"); @@ -115,9 +116,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.Owner.StringId); - responseDocument.SingleData.Attributes["displayName"].Should().Be(blog.Owner.DisplayName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.Owner.StringId); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().Be(blog.Owner.DisplayName); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("blogPosts"); @@ -147,9 +148,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[0].StringId); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(blog.Posts[0].Caption); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[0].StringId); + responseDocument.Data.ManyValue[0].Attributes["caption"].Should().Be(blog.Posts[0].Caption); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("webAccounts"); @@ -179,9 +180,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(comment.StringId); - responseDocument.SingleData.Attributes["text"].Should().Be(comment.Text); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(comment.StringId); + responseDocument.Data.SingleValue.Attributes["text"].Should().Be(comment.Text); responseDocument.Included.Should().HaveCount(2); @@ -215,14 +216,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("comments"); responseDocument.Included[0].Id.Should().Be(post.Comments.Single().StringId); - responseDocument.Included[0].Attributes["createdAt"].Should().BeCloseTo(post.Comments.Single().CreatedAt); + responseDocument.Included[0].Attributes["createdAt"].As().Should().BeCloseTo(post.Comments.Single().CreatedAt); } [Fact] @@ -246,9 +247,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("labels"); @@ -277,10 +278,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Type.Should().Be("labels"); - responseDocument.ManyData[0].Id.Should().Be(post.Labels.ElementAt(0).StringId); - responseDocument.ManyData[0].Attributes["name"].Should().Be(post.Labels.Single().Name); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Type.Should().Be("labels"); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.Labels.ElementAt(0).StringId); + responseDocument.Data.ManyValue[0].Attributes["name"].Should().Be(post.Labels.Single().Name); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("blogPosts"); @@ -311,9 +312,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(comment.StringId); - responseDocument.SingleData.Attributes["text"].Should().Be(comment.Text); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(comment.StringId); + responseDocument.Data.SingleValue.Attributes["text"].Should().Be(comment.Text); responseDocument.Included.Should().HaveCount(3); @@ -352,9 +353,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.StringId); - responseDocument.SingleData.Attributes["title"].Should().Be(blog.Title); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(blog.Title); responseDocument.Included.Should().HaveCount(2); @@ -364,7 +365,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[1].Type.Should().Be("comments"); responseDocument.Included[1].Id.Should().Be(blog.Posts[0].Comments.Single().StringId); - responseDocument.Included[1].Attributes["createdAt"].Should().BeCloseTo(blog.Posts[0].Comments.Single().CreatedAt); + responseDocument.Included[1].Attributes["createdAt"].As().Should().BeCloseTo(blog.Posts[0].Comments.Single().CreatedAt); } [Fact] @@ -391,9 +392,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(comment.StringId); - responseDocument.SingleData.Attributes["text"].Should().Be(comment.Text); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(comment.StringId); + responseDocument.Data.SingleValue.Attributes["text"].Should().Be(comment.Text); responseDocument.Included.Should().HaveCount(5); @@ -444,9 +445,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.StringId); - responseDocument.SingleData.Attributes["title"].Should().Be(blog.Title); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(blog.Title); responseDocument.Included.Should().HaveCount(7); @@ -503,9 +504,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("webAccounts"); @@ -538,7 +539,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("webAccounts"); @@ -550,20 +551,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_include_unknown_relationship() { // Arrange - const string route = "/webAccounts?include=doesNotExist"; + string route = $"/webAccounts?include={Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified include is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource 'webAccounts'."); error.Source.Parameter.Should().Be("include"); } @@ -571,20 +572,20 @@ public async Task Cannot_include_unknown_relationship() public async Task Cannot_include_unknown_nested_relationship() { // Arrange - const string route = "/blogs?include=posts.doesNotExist"; + string route = $"/blogs?include=posts.{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified include is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource 'blogPosts'."); error.Source.Parameter.Should().Be("include"); } @@ -595,14 +596,14 @@ public async Task Cannot_include_relationship_with_blocked_capability() const string route = "/blogPosts?include=parent"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Including the requested relationship is not allowed."); error.Detail.Should().Be("Including the relationship 'parent' on 'blogPosts' is not allowed."); @@ -631,16 +632,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - ResourceObject[] postWithReviewer = responseDocument.ManyData - .Where(resource => resource.Relationships.First(pair => pair.Key == "reviewer").Value.SingleData != null).ToArray(); + ResourceObject[] postWithReviewer = responseDocument.Data.ManyValue + .Where(resource => resource.Relationships.First(pair => pair.Key == "reviewer").Value.Data.SingleValue != null).ToArray(); postWithReviewer.Should().HaveCount(1); postWithReviewer[0].Attributes["caption"].Should().Be(posts[0].Caption); - ResourceObject[] postWithoutReviewer = responseDocument.ManyData - .Where(resource => resource.Relationships.First(pair => pair.Key == "reviewer").Value.SingleData == null).ToArray(); + ResourceObject[] postWithoutReviewer = responseDocument.Data.ManyValue + .Where(resource => resource.Relationships.First(pair => pair.Key == "reviewer").Value.Data.SingleValue == null).ToArray(); postWithoutReviewer.Should().HaveCount(1); postWithoutReviewer[0].Attributes["caption"].Should().Be(posts[1].Caption); @@ -685,14 +686,14 @@ public async Task Cannot_exceed_configured_maximum_inclusion_depth() const string route = "/blogs/123/owner?include=posts.comments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified include is invalid."); error.Detail.Should().Be("Including 'posts.comments' exceeds the maximum inclusion depth of 1."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/LabelColor.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/LabelColor.cs index 344faf6a0c..5c888227e7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/LabelColor.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/LabelColor.cs @@ -1,8 +1,10 @@ +using System.Text.Json.Serialization; using JetBrains.Annotations; namespace JsonApiDotNetCoreTests.IntegrationTests.QueryStrings { [UsedImplicitly(ImplicitUseTargetFlags.Members)] + [JsonConverter(typeof(JsonStringEnumMemberConverter))] public enum LabelColor { Red, diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs index 9d015e7fb6..8d87adfc7d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs @@ -60,12 +60,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?page[size]=1"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts?page[size]=1"); responseDocument.Links.Last.Should().Be(responseDocument.Links.Self); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); @@ -86,14 +86,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/blogPosts/{post.StringId}?page[number]=2"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); @@ -121,15 +121,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + $"/blogs/{blog.StringId}/posts?page[size]=1"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogs/{blog.StringId}/posts?page[size]=1"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); - responseDocument.Links.Next.Should().Be(HostPrefix + $"/blogs/{blog.StringId}/posts?page[number]=3&page[size]=1"); + responseDocument.Links.Next.Should().Be($"{HostPrefix}/blogs/{blog.StringId}/posts?page[number]=3&page[size]=1"); } [Fact] @@ -147,14 +147,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/blogPosts/{post.StringId}/author?page[size]=5"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); @@ -184,16 +184,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(blogs[0].Posts[1].StringId); responseDocument.Included[1].Id.Should().Be(blogs[1].Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + "/blogs?include=posts&page[size]=2,posts:1"); - responseDocument.Links.Last.Should().Be(HostPrefix + "/blogs?include=posts&page[number]=2&page[size]=2,posts:1"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogs?include=posts&page[size]=2,posts:1"); + responseDocument.Links.Last.Should().Be($"{HostPrefix}/blogs?include=posts&page[number]=2&page[size]=2,posts:1"); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().Be(responseDocument.Links.Last); } @@ -220,12 +220,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(blog.Owner.Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); @@ -253,12 +253,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + $"/blogs/{blog.StringId}/relationships/posts?page[size]=1"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogs/{blog.StringId}/relationships/posts?page[size]=1"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); @@ -292,15 +292,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(posts[0].Labels.ElementAt(1).StringId); responseDocument.Included[1].Id.Should().Be(posts[1].Labels.ElementAt(1).StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?include=labels&page[size]=labels:1"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts?include=labels&page[size]=labels:1"); responseDocument.Links.Last.Should().Be(responseDocument.Links.First); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); @@ -328,12 +328,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.Labels.ElementAt(1).StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.Labels.ElementAt(1).StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + $"/blogPosts/{post.StringId}/relationships/labels?page[size]=1"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts/{post.StringId}/relationships/labels?page[size]=1"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); @@ -364,20 +364,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); responseDocument.Included.Should().HaveCount(3); responseDocument.Included[0].Id.Should().Be(blogs[1].Owner.StringId); responseDocument.Included[1].Id.Should().Be(blogs[1].Owner.Posts[1].StringId); responseDocument.Included[2].Id.Should().Be(blogs[1].Owner.Posts[1].Comments.ElementAt(1).StringId); - const string linkPrefix = HostPrefix + "/blogs?include=owner.posts.comments"; + string linkPrefix = $"{HostPrefix}/blogs?include=owner.posts.comments"; responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(linkPrefix + "&page[size]=1,owner.posts:1,owner.posts.comments:1"); - responseDocument.Links.Last.Should().Be(linkPrefix + "&page[size]=1,owner.posts:1,owner.posts.comments:1&page[number]=2"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{linkPrefix}&page[size]=1,owner.posts:1,owner.posts.comments:1"); + responseDocument.Links.Last.Should().Be($"{linkPrefix}&page[size]=1,owner.posts:1,owner.posts.comments:1&page[number]=2"); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); } @@ -386,20 +386,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_paginate_in_unknown_scope() { // Arrange - const string route = "/webAccounts?page[number]=doesNotExist:1"; + string route = $"/webAccounts?page[number]={Unknown.Relationship}:1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource 'webAccounts'."); error.Source.Parameter.Should().Be("page[number]"); } @@ -407,20 +407,20 @@ public async Task Cannot_paginate_in_unknown_scope() public async Task Cannot_paginate_in_unknown_nested_scope() { // Arrange - const string route = "/webAccounts?page[size]=posts.doesNotExist:1"; + string route = $"/webAccounts?page[size]=posts.{Unknown.Relationship}:1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource 'blogPosts'."); error.Source.Parameter.Should().Be("page[size]"); } @@ -448,16 +448,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(blog.Posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[0].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(blog.Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.First.Should().Be(responseDocument.Links.Self); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); - responseDocument.Links.Next.Should().Be(HostPrefix + $"/blogs/{blog.StringId}/posts?page[number]=2"); + responseDocument.Links.Next.Should().Be($"{HostPrefix}/blogs/{blog.StringId}/posts?page[number]=2"); } [Fact] @@ -484,10 +484,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(25); + responseDocument.Data.ManyValue.Should().HaveCount(25); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); @@ -503,7 +503,7 @@ public async Task Renders_correct_top_level_links_for_page_number(int pageNumber { // Arrange WebAccount account = _fakers.WebAccount.Generate(); - account.UserName = "&" + account.UserName; + account.UserName = $"&{account.UserName}"; const int totalCount = 3 * DefaultPageSize + 3; List posts = _fakers.BlogPost.Generate(totalCount); @@ -520,10 +520,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string routePrefix = "/blogPosts?filter=equals(author.userName,'" + WebUtility.UrlEncode(account.UserName) + "')" + + string routePrefix = $"/blogPosts?filter=equals(author.userName,'{WebUtility.UrlEncode(account.UserName)}')" + "&fields[webAccounts]=userName&include=author&sort=id&foo=bar,baz"; - string route = routePrefix + $"&page[number]={pageNumber}"; + string route = $"{routePrefix}&page[number]={pageNumber}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -531,11 +531,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); if (firstLink != null) { - string expected = HostPrefix + SetPageNumberInUrl(routePrefix, firstLink.Value); + string expected = $"{HostPrefix}{SetPageNumberInUrl(routePrefix, firstLink.Value)}"; responseDocument.Links.First.Should().Be(expected); } else @@ -545,7 +545,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => if (prevLink != null) { - string expected = HostPrefix + SetPageNumberInUrl(routePrefix, prevLink.Value); + string expected = $"{HostPrefix}{SetPageNumberInUrl(routePrefix, prevLink.Value)}"; responseDocument.Links.Prev.Should().Be(expected); } else @@ -555,7 +555,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => if (nextLink != null) { - string expected = HostPrefix + SetPageNumberInUrl(routePrefix, nextLink.Value); + string expected = $"{HostPrefix}{SetPageNumberInUrl(routePrefix, nextLink.Value)}"; responseDocument.Links.Next.Should().Be(expected); } else @@ -565,7 +565,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => if (lastLink != null) { - string expected = HostPrefix + SetPageNumberInUrl(routePrefix, lastLink.Value); + string expected = $"{HostPrefix}{SetPageNumberInUrl(routePrefix, lastLink.Value)}"; responseDocument.Links.Last.Should().Be(expected); } else @@ -575,7 +575,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => static string SetPageNumberInUrl(string url, int pageNumber) { - return pageNumber != 1 ? url + "&page[number]=" + pageNumber : url; + return pageNumber != 1 ? $"{url}&page[number]={pageNumber}" : url; } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithoutTotalCountTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithoutTotalCountTests.cs index 9ff4accd59..89ebae123e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithoutTotalCountTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithoutTotalCountTests.cs @@ -48,7 +48,7 @@ public async Task Hides_pagination_links_when_unconstrained_page_size() httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); @@ -77,8 +77,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); @@ -103,10 +103,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?foo=bar"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts?foo=bar"); responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be(HostPrefix + "/blogPosts?foo=bar"); + responseDocument.Links.Prev.Should().Be($"{HostPrefix}/blogPosts?foo=bar"); responseDocument.Links.Next.Should().BeNull(); } @@ -131,13 +131,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Count.Should().BeLessThan(DefaultPageSize); + responseDocument.Data.ManyValue.Count.Should().BeLessThan(DefaultPageSize); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?foo=bar"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts?foo=bar"); responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be(HostPrefix + "/blogPosts?foo=bar&page[number]=2"); + responseDocument.Links.Prev.Should().Be($"{HostPrefix}/blogPosts?foo=bar&page[number]=2"); responseDocument.Links.Next.Should().BeNull(); } @@ -162,14 +162,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(DefaultPageSize); + responseDocument.Data.ManyValue.Should().HaveCount(DefaultPageSize); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?foo=bar"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts?foo=bar"); responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be(HostPrefix + "/blogPosts?page[number]=2&foo=bar"); - responseDocument.Links.Next.Should().Be(HostPrefix + "/blogPosts?page[number]=4&foo=bar"); + responseDocument.Links.Prev.Should().Be($"{HostPrefix}/blogPosts?page[number]=2&foo=bar"); + responseDocument.Links.Next.Should().Be($"{HostPrefix}/blogPosts?page[number]=4&foo=bar"); } [Fact] @@ -193,14 +193,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(DefaultPageSize); + responseDocument.Data.ManyValue.Should().HaveCount(DefaultPageSize); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + $"/webAccounts/{account.StringId}/posts?foo=bar"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/webAccounts/{account.StringId}/posts?foo=bar"); responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be(HostPrefix + $"/webAccounts/{account.StringId}/posts?page[number]=2&foo=bar"); - responseDocument.Links.Next.Should().Be(HostPrefix + $"/webAccounts/{account.StringId}/posts?page[number]=4&foo=bar"); + responseDocument.Links.Prev.Should().Be($"{HostPrefix}/webAccounts/{account.StringId}/posts?page[number]=2&foo=bar"); + responseDocument.Links.Next.Should().Be($"{HostPrefix}/webAccounts/{account.StringId}/posts?page[number]=4&foo=bar"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs index c6f576c94d..6186df580e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs @@ -36,14 +36,14 @@ public async Task Cannot_use_negative_page_number() const string route = "/blogs?page[number]=-1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be("Page number cannot be negative or zero."); @@ -57,14 +57,14 @@ public async Task Cannot_use_zero_page_number() const string route = "/blogs?page[number]=0"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be("Page number cannot be negative or zero."); @@ -105,7 +105,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().BeEmpty(); + responseDocument.Data.ManyValue.Should().BeEmpty(); } [Fact] @@ -115,14 +115,14 @@ public async Task Cannot_use_negative_page_size() const string route = "/blogs?page[size]=-1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be("Page size cannot be negative."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationWithMaximumTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationWithMaximumTests.cs index 583ae0d712..6d10840d7f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationWithMaximumTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationWithMaximumTests.cs @@ -33,7 +33,7 @@ public async Task Can_use_page_number_below_maximum() { // Arrange const int pageNumber = MaximumPageNumber - 1; - string route = "/blogs?page[number]=" + pageNumber; + string route = $"/blogs?page[number]={pageNumber}"; // Act (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); @@ -47,7 +47,7 @@ public async Task Can_use_page_number_equal_to_maximum() { // Arrange const int pageNumber = MaximumPageNumber; - string route = "/blogs?page[number]=" + pageNumber; + string route = $"/blogs?page[number]={pageNumber}"; // Act (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); @@ -61,17 +61,17 @@ public async Task Cannot_use_page_number_over_maximum() { // Arrange const int pageNumber = MaximumPageNumber + 1; - string route = "/blogs?page[number]=" + pageNumber; + string route = $"/blogs?page[number]={pageNumber}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be($"Page number cannot be higher than {MaximumPageNumber}."); @@ -85,14 +85,14 @@ public async Task Cannot_use_zero_page_size() const string route = "/blogs?page[size]=0"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be("Page size cannot be unconstrained."); @@ -104,10 +104,10 @@ public async Task Can_use_page_size_below_maximum() { // Arrange const int pageSize = MaximumPageSize - 1; - string route = "/blogs?page[size]=" + pageSize; + string route = $"/blogs?page[size]={pageSize}"; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -118,10 +118,10 @@ public async Task Can_use_page_size_equal_to_maximum() { // Arrange const int pageSize = MaximumPageSize; - string route = "/blogs?page[size]=" + pageSize; + string route = $"/blogs?page[size]={pageSize}"; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -132,17 +132,17 @@ public async Task Cannot_use_page_size_over_maximum() { // Arrange const int pageSize = MaximumPageSize + 1; - string route = "/blogs?page[size]=" + pageSize; + string route = $"/blogs?page[size]={pageSize}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be($"Page size cannot be higher than {MaximumPageSize}."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringTests.cs index 98c9bd85fa..eae4e173f3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringTests.cs @@ -31,14 +31,14 @@ public async Task Cannot_use_unknown_query_string_parameter() const string route = "/calendars?foo=bar"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Unknown query string parameter."); @@ -70,25 +70,23 @@ public async Task Can_use_unknown_query_string_parameter() [InlineData("sort")] [InlineData("page[size]")] [InlineData("page[number]")] - [InlineData("defaults")] - [InlineData("nulls")] public async Task Cannot_use_empty_query_string_parameter_value(string parameterName) { // Arrange var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); options.AllowUnknownQueryStringParameters = false; - string route = "calendars?" + parameterName + "="; + string route = $"calendars?{parameterName}="; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing query string parameter value."); error.Detail.Should().Be($"Missing value for '{parameterName}' query string parameter."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerDefaultValueHandlingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerDefaultValueHandlingTests.cs deleted file mode 100644 index 6ac9eb4ac1..0000000000 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerDefaultValueHandlingTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using TestBuildingBlocks; -using Xunit; - -namespace JsonApiDotNetCoreTests.IntegrationTests.QueryStrings -{ - public sealed class SerializerDefaultValueHandlingTests : IClassFixture, QueryStringDbContext>> - { - private readonly IntegrationTestContext, QueryStringDbContext> _testContext; - private readonly QueryStringFakers _fakers = new(); - - public SerializerDefaultValueHandlingTests(IntegrationTestContext, QueryStringDbContext> testContext) - { - _testContext = testContext; - - testContext.UseController(); - } - - [Fact] - public async Task Cannot_override_from_query_string() - { - // Arrange - var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); - options.AllowQueryStringOverrideForSerializerDefaultValueHandling = false; - - const string route = "/calendars?defaults=true"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - error.Detail.Should().Be("The parameter 'defaults' cannot be used at this endpoint."); - error.Source.Parameter.Should().Be("defaults"); - } - - [Theory] - [InlineData(null, null, true)] - [InlineData(null, "false", false)] - [InlineData(null, "true", true)] - [InlineData(DefaultValueHandling.Ignore, null, false)] - [InlineData(DefaultValueHandling.Ignore, "false", false)] - [InlineData(DefaultValueHandling.Ignore, "true", true)] - [InlineData(DefaultValueHandling.Include, null, true)] - [InlineData(DefaultValueHandling.Include, "false", false)] - [InlineData(DefaultValueHandling.Include, "true", true)] - public async Task Can_override_from_query_string(DefaultValueHandling? configurationValue, string queryStringValue, bool expectInDocument) - { - // Arrange - var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); - options.AllowQueryStringOverrideForSerializerDefaultValueHandling = true; - options.SerializerSettings.DefaultValueHandling = configurationValue ?? DefaultValueHandling.Include; - - Calendar calendar = _fakers.Calendar.Generate(); - calendar.DefaultAppointmentDurationInMinutes = default; - calendar.Appointments = _fakers.Appointment.Generate(1).ToHashSet(); - calendar.Appointments.Single().EndTime = default; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.Calendars.Add(calendar); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/calendars/{calendar.StringId}?include=appointments" + (queryStringValue != null ? "&defaults=" + queryStringValue : ""); - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.Included.Should().HaveCount(1); - - if (expectInDocument) - { - responseDocument.SingleData.Attributes.Should().ContainKey("defaultAppointmentDurationInMinutes"); - responseDocument.Included[0].Attributes.Should().ContainKey("endTime"); - } - else - { - responseDocument.SingleData.Attributes.Should().NotContainKey("defaultAppointmentDurationInMinutes"); - responseDocument.Included[0].Attributes.Should().NotContainKey("endTime"); - } - } - } -} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerIgnoreConditionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerIgnoreConditionTests.cs new file mode 100644 index 0000000000..e8d790446c --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerIgnoreConditionTests.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Extensions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using TestBuildingBlocks; +using Xunit; + +namespace JsonApiDotNetCoreTests.IntegrationTests.QueryStrings +{ + public sealed class SerializerIgnoreConditionTests : IntegrationTestContext, QueryStringDbContext> + { + private readonly QueryStringFakers _fakers = new(); + + public SerializerIgnoreConditionTests() + { + UseController(); + } + + [Theory] + [InlineData(JsonIgnoreCondition.Never, true, true)] + [InlineData(JsonIgnoreCondition.WhenWritingDefault, false, false)] + [InlineData(JsonIgnoreCondition.WhenWritingNull, false, true)] + public async Task Applies_configuration_for_ignore_condition(JsonIgnoreCondition configurationValue, bool expectNullValueInDocument, + bool expectDefaultValueInDocument) + { + // Arrange + var options = (JsonApiOptions)Factory.Services.GetRequiredService(); + options.SerializerOptions.DefaultIgnoreCondition = configurationValue; + + Calendar calendar = _fakers.Calendar.Generate(); + calendar.TimeZone = null; + calendar.DefaultAppointmentDurationInMinutes = default; + calendar.ShowWeekNumbers = true; + calendar.Appointments = _fakers.Appointment.Generate(1).ToHashSet(); + calendar.Appointments.Single().Title = null; + calendar.Appointments.Single().StartTime = default; + calendar.Appointments.Single().EndTime = 1.January(2001); + + await RunOnDatabaseAsync(async dbContext => + { + dbContext.Calendars.Add(calendar); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/calendars/{calendar.StringId}?include=appointments"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Included.Should().HaveCount(1); + + if (expectNullValueInDocument) + { + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("timeZone"); + responseDocument.Included[0].Attributes.Should().ContainKey("title"); + } + else + { + responseDocument.Data.SingleValue.Attributes.Should().NotContainKey("timeZone"); + responseDocument.Included[0].Attributes.Should().NotContainKey("title"); + } + + if (expectDefaultValueInDocument) + { + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("defaultAppointmentDurationInMinutes"); + responseDocument.Included[0].Attributes.Should().ContainKey("startTime"); + } + else + { + responseDocument.Data.SingleValue.Attributes.Should().NotContainKey("defaultAppointmentDurationInMinutes"); + responseDocument.Included[0].Attributes.Should().NotContainKey("startTime"); + } + } + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerNullValueHandlingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerNullValueHandlingTests.cs deleted file mode 100644 index 3217e06c2a..0000000000 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerNullValueHandlingTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using TestBuildingBlocks; -using Xunit; - -namespace JsonApiDotNetCoreTests.IntegrationTests.QueryStrings -{ - public sealed class SerializerNullValueHandlingTests : IClassFixture, QueryStringDbContext>> - { - private readonly IntegrationTestContext, QueryStringDbContext> _testContext; - private readonly QueryStringFakers _fakers = new(); - - public SerializerNullValueHandlingTests(IntegrationTestContext, QueryStringDbContext> testContext) - { - _testContext = testContext; - - testContext.UseController(); - } - - [Fact] - public async Task Cannot_override_from_query_string() - { - // Arrange - var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); - options.AllowQueryStringOverrideForSerializerNullValueHandling = false; - - const string route = "/calendars?nulls=true"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - error.Detail.Should().Be("The parameter 'nulls' cannot be used at this endpoint."); - error.Source.Parameter.Should().Be("nulls"); - } - - [Theory] - [InlineData(null, null, true)] - [InlineData(null, "false", false)] - [InlineData(null, "true", true)] - [InlineData(NullValueHandling.Ignore, null, false)] - [InlineData(NullValueHandling.Ignore, "false", false)] - [InlineData(NullValueHandling.Ignore, "true", true)] - [InlineData(NullValueHandling.Include, null, true)] - [InlineData(NullValueHandling.Include, "false", false)] - [InlineData(NullValueHandling.Include, "true", true)] - public async Task Can_override_from_query_string(NullValueHandling? configurationValue, string queryStringValue, bool expectInDocument) - { - // Arrange - var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); - options.AllowQueryStringOverrideForSerializerNullValueHandling = true; - options.SerializerSettings.NullValueHandling = configurationValue ?? NullValueHandling.Include; - - Calendar calendar = _fakers.Calendar.Generate(); - calendar.TimeZone = null; - calendar.Appointments = _fakers.Appointment.Generate(1).ToHashSet(); - calendar.Appointments.Single().Title = null; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.Calendars.Add(calendar); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/calendars/{calendar.StringId}?include=appointments" + (queryStringValue != null ? "&nulls=" + queryStringValue : ""); - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.Included.Should().HaveCount(1); - - if (expectInDocument) - { - responseDocument.SingleData.Attributes.Should().ContainKey("timeZone"); - responseDocument.Included[0].Attributes.Should().ContainKey("title"); - } - else - { - responseDocument.SingleData.Attributes.Should().NotContainKey("timeZone"); - responseDocument.Included[0].Attributes.Should().NotContainKey("title"); - } - } - } -} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs index c70b793b59..43ceb3fdf9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs @@ -49,10 +49,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(posts[0].StringId); - responseDocument.ManyData[2].Id.Should().Be(posts[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(posts[0].StringId); + responseDocument.Data.ManyValue[2].Id.Should().Be(posts[2].StringId); } [Fact] @@ -70,14 +70,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/blogPosts/{post.StringId}?sort=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified sort is invalid."); error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); @@ -108,10 +108,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(blog.Posts[0].StringId); - responseDocument.ManyData[2].Id.Should().Be(blog.Posts[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(blog.Posts[0].StringId); + responseDocument.Data.ManyValue[2].Id.Should().Be(blog.Posts[2].StringId); } [Fact] @@ -129,14 +129,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/blogPosts/{post.StringId}/author?sort=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified sort is invalid."); error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); @@ -166,9 +166,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(blogs[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(blogs[0].StringId); } [Fact] @@ -194,9 +194,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(posts[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(posts[0].StringId); } [Fact] @@ -223,8 +223,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(account.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(account.StringId); responseDocument.Included.Should().HaveCount(3); responseDocument.Included[0].Id.Should().Be(account.Posts[1].StringId); @@ -257,8 +257,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.Owner.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.Owner.StringId); responseDocument.Included.Should().HaveCount(3); responseDocument.Included[0].Id.Should().Be(blog.Owner.Posts[1].StringId); @@ -290,8 +290,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); responseDocument.Included.Should().HaveCount(3); responseDocument.Included[0].Id.Should().Be(post.Labels.ElementAt(1).StringId); @@ -337,9 +337,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(blogs[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(blogs[0].StringId); responseDocument.Included.Should().HaveCount(7); @@ -391,9 +391,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(posts[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(posts[0].StringId); } [Fact] @@ -428,9 +428,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(blogs[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(blogs[0].StringId); responseDocument.Included.Should().HaveCount(5); responseDocument.Included[0].Id.Should().Be(blogs[1].Owner.StringId); @@ -444,42 +444,42 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_sort_in_unknown_scope() { // Arrange - const string route = "/webAccounts?sort[doesNotExist]=id"; + string route = $"/webAccounts?sort[{Unknown.Relationship}]=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified sort is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); - error.Source.Parameter.Should().Be("sort[doesNotExist]"); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource 'webAccounts'."); + error.Source.Parameter.Should().Be($"sort[{Unknown.Relationship}]"); } [Fact] public async Task Cannot_sort_in_unknown_nested_scope() { // Arrange - const string route = "/webAccounts?sort[posts.doesNotExist]=id"; + string route = $"/webAccounts?sort[posts.{Unknown.Relationship}]=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified sort is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); - error.Source.Parameter.Should().Be("sort[posts.doesNotExist]"); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource 'blogPosts'."); + error.Source.Parameter.Should().Be($"sort[posts.{Unknown.Relationship}]"); } [Fact] @@ -489,14 +489,14 @@ public async Task Cannot_sort_on_attribute_with_blocked_capability() const string route = "/webAccounts?sort=dateOfBirth"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Sorting on the requested attribute is not allowed."); error.Detail.Should().Be("Sorting on attribute 'dateOfBirth' is not allowed."); @@ -531,10 +531,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(accounts[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(accounts[2].StringId); - responseDocument.ManyData[2].Id.Should().Be(accounts[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(accounts[2].StringId); + responseDocument.Data.ManyValue[2].Id.Should().Be(accounts[0].StringId); } [Fact] @@ -562,11 +562,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(4); - responseDocument.ManyData[0].Id.Should().Be(accounts[2].StringId); - responseDocument.ManyData[1].Id.Should().Be(accounts[1].StringId); - responseDocument.ManyData[2].Id.Should().Be(accounts[0].StringId); - responseDocument.ManyData[3].Id.Should().Be(accounts[3].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(4); + responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[2].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(accounts[1].StringId); + responseDocument.Data.ManyValue[2].Id.Should().Be(accounts[0].StringId); + responseDocument.Data.ManyValue[3].Id.Should().Be(accounts[3].StringId); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs index 14ac363c02..a8681a1403 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Net; using System.Net.Http; @@ -58,14 +59,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(post.Caption); - responseDocument.ManyData[0].Relationships.Should().HaveCount(1); - responseDocument.ManyData[0].Relationships["author"].Data.Should().BeNull(); - responseDocument.ManyData[0].Relationships["author"].Links.Self.Should().NotBeNull(); - responseDocument.ManyData[0].Relationships["author"].Links.Related.Should().NotBeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.ManyValue[0].Relationships.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Relationships["author"].Data.Value.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships["author"].Links.Self.Should().NotBeNull(); + responseDocument.Data.ManyValue[0].Relationships["author"].Links.Related.Should().NotBeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Caption.Should().Be(post.Caption); @@ -96,11 +97,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(post.Caption); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Caption.Should().Be(post.Caption); @@ -131,13 +132,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.StringId); - responseDocument.ManyData[0].Attributes.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().HaveCount(1); - responseDocument.ManyData[0].Relationships["author"].Data.Should().BeNull(); - responseDocument.ManyData[0].Relationships["author"].Links.Self.Should().NotBeNull(); - responseDocument.ManyData[0].Relationships["author"].Links.Related.Should().NotBeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Relationships["author"].Data.Value.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships["author"].Links.Self.Should().NotBeNull(); + responseDocument.Data.ManyValue[0].Relationships["author"].Links.Related.Should().NotBeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Caption.Should().BeNull(); @@ -168,14 +169,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[0].StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(blog.Posts[0].Caption); - responseDocument.ManyData[0].Relationships.Should().HaveCount(1); - responseDocument.ManyData[0].Relationships["labels"].Data.Should().BeNull(); - responseDocument.ManyData[0].Relationships["labels"].Links.Self.Should().NotBeNull(); - responseDocument.ManyData[0].Relationships["labels"].Links.Related.Should().NotBeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[0].StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["caption"].Should().Be(blog.Posts[0].Caption); + responseDocument.Data.ManyValue[0].Relationships.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Relationships["labels"].Data.Value.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships["labels"].Links.Self.Should().NotBeNull(); + responseDocument.Data.ManyValue[0].Relationships["labels"].Links.Related.Should().NotBeNull(); var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).And.Subject.Single(); blogCaptured.Id.Should().Be(blog.Id); @@ -209,14 +210,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["url"].Should().Be(post.Url); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["author"].Data.Should().BeNull(); - responseDocument.SingleData.Relationships["author"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["author"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["url"].Should().Be(post.Url); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["author"].Data.Value.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships["author"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["author"].Links.Related.Should().NotBeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Url.Should().Be(post.Url); @@ -247,19 +248,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); - responseDocument.SingleData.Relationships["author"].SingleData.Id.Should().Be(post.Author.StringId); - responseDocument.SingleData.Relationships["author"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["author"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Relationships["author"].Data.SingleValue.Id.Should().Be(post.Author.StringId); + responseDocument.Data.SingleValue.Relationships["author"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["author"].Links.Related.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Attributes.Should().HaveCount(2); responseDocument.Included[0].Attributes["displayName"].Should().Be(post.Author.DisplayName); responseDocument.Included[0].Attributes["emailAddress"].Should().Be(post.Author.EmailAddress); responseDocument.Included[0].Relationships.Should().HaveCount(1); - responseDocument.Included[0].Relationships["preferences"].Data.Should().BeNull(); + responseDocument.Included[0].Relationships["preferences"].Data.Value.Should().BeNull(); responseDocument.Included[0].Relationships["preferences"].Links.Self.Should().NotBeNull(); responseDocument.Included[0].Relationships["preferences"].Links.Related.Should().NotBeNull(); @@ -296,19 +297,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(account.StringId); - responseDocument.SingleData.Attributes["displayName"].Should().Be(account.DisplayName); - responseDocument.SingleData.Relationships["posts"].ManyData.Should().HaveCount(1); - responseDocument.SingleData.Relationships["posts"].ManyData[0].Id.Should().Be(account.Posts[0].StringId); - responseDocument.SingleData.Relationships["posts"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["posts"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(account.StringId); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().Be(account.DisplayName); + responseDocument.Data.SingleValue.Relationships["posts"].Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["posts"].Data.ManyValue[0].Id.Should().Be(account.Posts[0].StringId); + responseDocument.Data.SingleValue.Relationships["posts"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["posts"].Links.Related.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Attributes.Should().HaveCount(1); responseDocument.Included[0].Attributes["caption"].Should().Be(account.Posts[0].Caption); responseDocument.Included[0].Relationships.Should().HaveCount(1); - responseDocument.Included[0].Relationships["labels"].Data.Should().BeNull(); + responseDocument.Included[0].Relationships["labels"].Data.Value.Should().BeNull(); responseDocument.Included[0].Relationships["labels"].Links.Self.Should().NotBeNull(); responseDocument.Included[0].Relationships["labels"].Links.Related.Should().NotBeNull(); @@ -346,19 +347,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.Owner.StringId); - responseDocument.SingleData.Attributes["displayName"].Should().Be(blog.Owner.DisplayName); - responseDocument.SingleData.Relationships["posts"].ManyData.Should().HaveCount(1); - responseDocument.SingleData.Relationships["posts"].ManyData[0].Id.Should().Be(blog.Owner.Posts[0].StringId); - responseDocument.SingleData.Relationships["posts"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["posts"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.Owner.StringId); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().Be(blog.Owner.DisplayName); + responseDocument.Data.SingleValue.Relationships["posts"].Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["posts"].Data.ManyValue[0].Id.Should().Be(blog.Owner.Posts[0].StringId); + responseDocument.Data.SingleValue.Relationships["posts"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["posts"].Links.Related.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Attributes.Should().HaveCount(1); responseDocument.Included[0].Attributes["caption"].Should().Be(blog.Owner.Posts[0].Caption); responseDocument.Included[0].Relationships.Should().HaveCount(1); - responseDocument.Included[0].Relationships["comments"].Data.Should().BeNull(); + responseDocument.Included[0].Relationships["comments"].Data.Value.Should().BeNull(); responseDocument.Included[0].Relationships["comments"].Links.Self.Should().NotBeNull(); responseDocument.Included[0].Relationships["comments"].Links.Related.Should().NotBeNull(); @@ -396,17 +397,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); - responseDocument.SingleData.Relationships["labels"].ManyData.Should().HaveCount(1); - responseDocument.SingleData.Relationships["labels"].ManyData[0].Id.Should().Be(post.Labels.ElementAt(0).StringId); - responseDocument.SingleData.Relationships["labels"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["labels"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Relationships["labels"].Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["labels"].Data.ManyValue[0].Id.Should().Be(post.Labels.ElementAt(0).StringId); + responseDocument.Data.SingleValue.Relationships["labels"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["labels"].Links.Related.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Attributes.Should().HaveCount(1); - responseDocument.Included[0].Attributes["color"].Should().Be(post.Labels.Single().Color.ToString("G")); + responseDocument.Included[0].Attributes["color"].Should().Be(post.Labels.Single().Color); responseDocument.Included[0].Relationships.Should().BeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); @@ -443,11 +444,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["title"].Should().Be(blog.Title); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(blog.Title); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); responseDocument.Included.Should().HaveCount(2); @@ -501,30 +502,30 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["title"].Should().Be(blog.Title); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["owner"].SingleData.Id.Should().Be(blog.Owner.StringId); - responseDocument.SingleData.Relationships["owner"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["owner"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(blog.Title); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["owner"].Data.SingleValue.Id.Should().Be(blog.Owner.StringId); + responseDocument.Data.SingleValue.Relationships["owner"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["owner"].Links.Related.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(blog.Owner.StringId); responseDocument.Included[0].Attributes["userName"].Should().Be(blog.Owner.UserName); responseDocument.Included[0].Attributes["displayName"].Should().Be(blog.Owner.DisplayName); - responseDocument.Included[0].Attributes["dateOfBirth"].Should().BeCloseTo(blog.Owner.DateOfBirth); - responseDocument.Included[0].Relationships["posts"].ManyData.Should().HaveCount(1); - responseDocument.Included[0].Relationships["posts"].ManyData[0].Id.Should().Be(blog.Owner.Posts[0].StringId); + responseDocument.Included[0].Attributes["dateOfBirth"].As().Should().BeCloseTo(blog.Owner.DateOfBirth.GetValueOrDefault()); + responseDocument.Included[0].Relationships["posts"].Data.ManyValue.Should().HaveCount(1); + responseDocument.Included[0].Relationships["posts"].Data.ManyValue[0].Id.Should().Be(blog.Owner.Posts[0].StringId); responseDocument.Included[0].Relationships["posts"].Links.Self.Should().NotBeNull(); responseDocument.Included[0].Relationships["posts"].Links.Related.Should().NotBeNull(); responseDocument.Included[1].Id.Should().Be(blog.Owner.Posts[0].StringId); responseDocument.Included[1].Attributes["caption"].Should().Be(blog.Owner.Posts[0].Caption); responseDocument.Included[1].Attributes["url"].Should().Be(blog.Owner.Posts[0].Url); - responseDocument.Included[1].Relationships["labels"].Data.Should().BeNull(); + responseDocument.Included[1].Relationships["labels"].Data.Value.Should().BeNull(); responseDocument.Included[1].Relationships["labels"].Links.Self.Should().NotBeNull(); responseDocument.Included[1].Relationships["labels"].Links.Related.Should().NotBeNull(); @@ -558,11 +559,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(post.Caption); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Id.Should().Be(post.Id); @@ -594,10 +595,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.StringId); - responseDocument.ManyData[0].Attributes.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Id.Should().Be(post.Id); @@ -608,21 +609,21 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_select_on_unknown_resource_type() { // Arrange - const string route = "/webAccounts?fields[doesNotExist]=id"; + string route = $"/webAccounts?fields[{Unknown.ResourceType}]=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified fieldset is invalid."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - error.Source.Parameter.Should().Be("fields[doesNotExist]"); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + error.Source.Parameter.Should().Be($"fields[{Unknown.ResourceType}]"); } [Fact] @@ -634,14 +635,14 @@ public async Task Cannot_select_attribute_with_blocked_capability() string route = $"/webAccounts/{account.Id}?fields[webAccounts]=password"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Retrieving the requested attribute is not allowed."); error.Detail.Should().Be("Retrieving the attribute 'password' is not allowed."); @@ -671,11 +672,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["showAdvertisements"].Should().Be(blog.ShowAdvertisements); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["showAdvertisements"].Should().Be(blog.ShowAdvertisements); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).And.Subject.Single(); blogCaptured.ShowAdvertisements.Should().Be(blogCaptured.ShowAdvertisements); @@ -705,15 +706,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(2); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); - responseDocument.SingleData.Attributes["url"].Should().Be(post.Url); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["author"].Data.Should().BeNull(); - responseDocument.SingleData.Relationships["author"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["author"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(2); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Attributes["url"].Should().Be(post.Url); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["author"].Data.Value.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships["author"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["author"].Links.Related.Should().NotBeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Id.Should().Be(post.Id); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 6bbb3ce71c..9a148f48b1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -61,14 +61,14 @@ public async Task Sets_location_header_for_created_resource() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - string newWorkItemId = responseDocument.SingleData.Id; - httpResponse.Headers.Location.Should().Be("/workItems/" + newWorkItemId); + string newWorkItemId = responseDocument.Data.SingleValue.Id; + httpResponse.Headers.Location.Should().Be($"/workItems/{newWorkItemId}"); responseDocument.Links.Self.Should().Be("http://localhost/workItems"); responseDocument.Links.First.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be("http://localhost" + httpResponse.Headers.Location); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be($"http://localhost{httpResponse.Headers.Location}"); } [Fact] @@ -98,13 +98,13 @@ public async Task Can_create_resource_with_int_ID() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); - responseDocument.SingleData.Attributes["dueAt"].Should().Be(newWorkItem.DueAt); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(newWorkItem.Description); + responseDocument.Data.SingleValue.Attributes["dueAt"].Should().Be(newWorkItem.DueAt); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -145,13 +145,13 @@ public async Task Can_create_resource_with_long_ID() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("userAccounts"); - responseDocument.SingleData.Attributes["firstName"].Should().Be(newUserAccount.FirstName); - responseDocument.SingleData.Attributes["lastName"].Should().Be(newUserAccount.LastName); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("userAccounts"); + responseDocument.Data.SingleValue.Attributes["firstName"].Should().Be(newUserAccount.FirstName); + responseDocument.Data.SingleValue.Attributes["lastName"].Should().Be(newUserAccount.LastName); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - long newUserAccountId = long.Parse(responseDocument.SingleData.Id); + long newUserAccountId = long.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -191,12 +191,12 @@ public async Task Can_create_resource_with_guid_ID() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItemGroups"); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroup.Name); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItemGroups"); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(newGroup.Name); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + Guid newGroupId = Guid.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -235,13 +235,13 @@ public async Task Can_create_resource_without_attributes_or_relationships() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Attributes["description"].Should().BeNull(); - responseDocument.SingleData.Attributes["dueAt"].Should().BeNull(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Attributes["description"].Should().BeNull(); + responseDocument.Data.SingleValue.Attributes["dueAt"].Should().BeNull(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -279,12 +279,12 @@ public async Task Can_create_resource_with_unknown_attribute() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(newWorkItem.Description); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -309,8 +309,8 @@ public async Task Can_create_resource_with_unknown_relationship() { data = new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 } } } @@ -325,12 +325,12 @@ public async Task Can_create_resource_with_unknown_relationship() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -360,14 +360,14 @@ public async Task Cannot_create_resource_with_client_generated_ID() const string route = "/rgbColors"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Specifying the resource ID in POST requests is not allowed."); error.Detail.Should().BeNull(); @@ -383,14 +383,14 @@ public async Task Cannot_create_resource_for_missing_request_body() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -413,14 +413,14 @@ public async Task Cannot_create_resource_for_missing_type() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); @@ -434,7 +434,7 @@ public async Task Cannot_create_resource_for_unknown_type() { data = new { - type = "doesNotExist", + type = Unknown.ResourceType, attributes = new { } @@ -444,17 +444,17 @@ public async Task Cannot_create_resource_for_unknown_type() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -472,7 +472,7 @@ public async Task Cannot_create_resource_on_unknown_resource_type_in_url() } }; - const string route = "/doesNotExist"; + const string route = "/" + Unknown.ResourceType; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -499,14 +499,14 @@ public async Task Cannot_create_on_resource_type_mismatch_between_url_and_body() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); error.Detail.Should().Be("Expected resource of type 'workItems' in POST request body at endpoint '/workItems', instead of 'rgbColors'."); @@ -531,17 +531,17 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Setting the initial value of the requested attribute is not allowed."); - error.Detail.Should().StartWith("Setting the initial value of 'isImportant' is not allowed. - Request body:"); + error.Detail.Should().StartWith("Setting the initial value of 'isImportant' is not allowed. - Request body: <<"); } [Fact] @@ -563,17 +563,17 @@ public async Task Cannot_create_resource_with_readonly_attribute() const string route = "/workItemGroups"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); - error.Detail.Should().StartWith("Attribute 'isDeprecated' is read-only. - Request body:"); + error.Detail.Should().StartWith("Attribute 'isDeprecated' is read-only. - Request body: <<"); } [Fact] @@ -585,17 +585,17 @@ public async Task Cannot_create_resource_for_broken_JSON_request_body() const string route = "/workItemGroups"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Invalid character after parsing"); + error.Detail.Should().Match("'{' is invalid after a property name. * - Request body: <<*"); } [Fact] @@ -617,17 +617,19 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'Nullable`1'. - Request body: <<"); + + error.Detail.Should().StartWith("Failed to convert attribute 'dueAt' with value 'not-a-valid-time' " + + "of type 'String' to type 'Nullable'. - Request body: <<"); } [Fact] @@ -699,11 +701,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["description"].Should().Be(newDescription); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(newDescription); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs index 3c30d86e83..e8dc30f79f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs @@ -63,17 +63,17 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItemGroups"); - responseDocument.SingleData.Id.Should().Be(newGroup.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroup.Name + ImplicitlyChangingWorkItemGroupDefinition.Suffix); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItemGroups"); + responseDocument.Data.SingleValue.Id.Should().Be(newGroup.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be($"{newGroup.Name}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { WorkItemGroup groupInDatabase = await dbContext.Groups.FirstWithIdAsync(newGroup.Id); - groupInDatabase.Name.Should().Be(newGroup.Name + ImplicitlyChangingWorkItemGroupDefinition.Suffix); + groupInDatabase.Name.Should().Be($"{newGroup.Name}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"); }); PropertyInfo property = typeof(WorkItemGroup).GetProperty(nameof(Identifiable.Id)); @@ -108,18 +108,18 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItemGroups"); - responseDocument.SingleData.Id.Should().Be(newGroup.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroup.Name + ImplicitlyChangingWorkItemGroupDefinition.Suffix); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItemGroups"); + responseDocument.Data.SingleValue.Id.Should().Be(newGroup.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be($"{newGroup.Name}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { WorkItemGroup groupInDatabase = await dbContext.Groups.FirstWithIdAsync(newGroup.Id); - groupInDatabase.Name.Should().Be(newGroup.Name + ImplicitlyChangingWorkItemGroupDefinition.Suffix); + groupInDatabase.Name.Should().Be($"{newGroup.Name}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"); }); PropertyInfo property = typeof(WorkItemGroup).GetProperty(nameof(Identifiable.Id)); @@ -237,14 +237,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/rgbColors"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Another resource with the specified ID already exists."); error.Detail.Should().Be($"Another resource of type 'rgbColors' with ID '{existingColor.StringId}' already exists."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index f9d1caa667..e9eae47336 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -72,12 +72,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().BeNull(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -136,9 +136,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(2); responseDocument.Included.Should().OnlyContain(resource => resource.Type == "userAccounts"); @@ -148,7 +148,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included.Should().OnlyContain(resource => resource.Attributes["lastName"] != null); responseDocument.Included.Should().OnlyContain(resource => resource.Relationships.Count > 0); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -207,9 +207,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(2); responseDocument.Included.Should().OnlyContain(resource => resource.Type == "userAccounts"); @@ -219,7 +219,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included.Should().OnlyContain(resource => resource.Attributes["firstName"] != null); responseDocument.Included.Should().OnlyContain(resource => resource.Relationships == null); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -289,14 +289,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["priority"].Should().Be(workItemToCreate.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["tags"].ManyData.Should().HaveCount(3); - responseDocument.SingleData.Relationships["tags"].ManyData[0].Id.Should().Be(existingTags[0].StringId); - responseDocument.SingleData.Relationships["tags"].ManyData[1].Id.Should().Be(existingTags[1].StringId); - responseDocument.SingleData.Relationships["tags"].ManyData[2].Id.Should().Be(existingTags[2].StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(workItemToCreate.Priority); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue[0].Id.Should().Be(existingTags[0].StringId); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue[1].Id.Should().Be(existingTags[1].StringId); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue[2].Id.Should().Be(existingTags[2].StringId); responseDocument.Included.Should().HaveCount(3); responseDocument.Included.Should().OnlyContain(resource => resource.Type == "workTags"); @@ -307,7 +307,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included.Should().OnlyContain(resource => resource.Attributes["text"] != null); responseDocument.Included.Should().OnlyContain(resource => resource.Relationships == null); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -337,7 +337,7 @@ public async Task Cannot_create_for_missing_relationship_type() { new { - id = 12345678 + id = Unknown.StringId.For() } } } @@ -348,14 +348,14 @@ public async Task Cannot_create_for_missing_relationship_type() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'subscribers' relationship. - Request body: <<"); @@ -378,8 +378,8 @@ public async Task Cannot_create_for_unknown_relationship_type() { new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -390,17 +390,17 @@ public async Task Cannot_create_for_unknown_relationship_type() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -431,14 +431,14 @@ public async Task Cannot_create_for_missing_relationship_ID() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Expected 'id' element in 'subscribers' relationship. - Request body: <<"); @@ -448,6 +448,9 @@ public async Task Cannot_create_for_missing_relationship_ID() public async Task Cannot_create_for_unknown_relationship_IDs() { // Arrange + string workItemId1 = Unknown.StringId.For(); + string workItemId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new @@ -462,12 +465,12 @@ public async Task Cannot_create_for_unknown_relationship_IDs() new { type = "workItems", - id = 12345678 + id = workItemId1 }, new { type = "workItems", - id = 87654321 + id = workItemId2 } } } @@ -478,22 +481,22 @@ public async Task Cannot_create_for_unknown_relationship_IDs() const string route = "/userAccounts"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'workItems' with ID '12345678' in relationship 'assignedItems' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'workItems' with ID '{workItemId1}' in relationship 'assignedItems' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'workItems' with ID '87654321' in relationship 'assignedItems' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'workItems' with ID '{workItemId2}' in relationship 'assignedItems' does not exist."); } [Fact] @@ -525,14 +528,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().StartWith("Relationship 'subscribers' contains incompatible resource type 'rgbColors'. - Request body: <<"); @@ -585,15 +588,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); responseDocument.Included[0].Id.Should().Be(existingUserAccount.StringId); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -626,14 +629,14 @@ public async Task Cannot_create_with_null_data_in_OneToMany_relationship() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); @@ -661,14 +664,14 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); @@ -706,16 +709,16 @@ public async Task Cannot_create_resource_with_local_ID() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Local IDs cannot be used at this endpoint."); + error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("Local IDs cannot be used at this endpoint. - Request body: <<"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index 761dfa50df..acd6f6136e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -2,13 +2,13 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; using TestBuildingBlocks; using Xunit; @@ -72,11 +72,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - string newGroupId = responseDocument.SingleData.Id; + string newGroupId = responseDocument.Data.SingleValue.Id; newGroupId.Should().NotBeNullOrEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -190,9 +190,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); @@ -201,7 +201,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Attributes["lastName"].Should().Be(existingUserAccount.LastName); responseDocument.Included[0].Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -257,11 +257,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["assignee"].SingleData.Id.Should().Be(existingUserAccount.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(newWorkItem.Description); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["assignee"].Data.SingleValue.Id.Should().Be(existingUserAccount.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); @@ -270,7 +270,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Attributes["lastName"].Should().Be(existingUserAccount.LastName); responseDocument.Included[0].Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -298,7 +298,7 @@ public async Task Cannot_create_for_missing_relationship_type() { data = new { - id = 12345678 + id = Unknown.StringId.For() } } } @@ -308,14 +308,14 @@ public async Task Cannot_create_for_missing_relationship_type() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'assignee' relationship. - Request body: <<"); @@ -336,8 +336,8 @@ public async Task Cannot_create_for_unknown_relationship_type() { data = new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -347,17 +347,17 @@ public async Task Cannot_create_for_unknown_relationship_type() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -385,14 +385,14 @@ public async Task Cannot_create_for_missing_relationship_ID() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Expected 'id' element in 'assignee' relationship. - Request body: <<"); @@ -402,6 +402,8 @@ public async Task Cannot_create_for_missing_relationship_ID() public async Task Cannot_create_with_unknown_relationship_ID() { // Arrange + string userAccountId = Unknown.StringId.For(); + var requestBody = new { data = new @@ -414,7 +416,7 @@ public async Task Cannot_create_with_unknown_relationship_ID() data = new { type = "userAccounts", - id = 12345678 + id = userAccountId } } } @@ -424,17 +426,17 @@ public async Task Cannot_create_with_unknown_relationship_ID() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'userAccounts' with ID '12345678' in relationship 'assignee' does not exist."); + error.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId}' in relationship 'assignee' does not exist."); } [Fact] @@ -463,14 +465,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().StartWith("Relationship 'assignee' contains incompatible resource type 'rgbColors'. - Request body: <<"); @@ -515,7 +517,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string requestBodyText = JsonConvert.SerializeObject(requestBody).Replace("assignee_duplicate", "assignee"); + string requestBodyText = JsonSerializer.Serialize(requestBody).Replace("assignee_duplicate", "assignee"); const string route = "/workItems?include=assignee"; @@ -525,9 +527,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); @@ -536,7 +538,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Attributes["lastName"].Should().Be(existingUserAccounts[1].LastName); responseDocument.Included[0].Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -584,14 +586,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().StartWith("Expected single data element for 'assignee' relationship. - Request body: <<"); @@ -626,16 +628,16 @@ public async Task Cannot_create_resource_with_local_ID() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Local IDs cannot be used at this endpoint."); + error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("Local IDs cannot be used at this endpoint. - Request body: <<"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs index f3a75b461d..e9d7116889 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs @@ -37,7 +37,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -56,23 +56,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_delete_missing_resource() + public async Task Cannot_delete_unknown_resource() { // Arrange - const string route = "/workItems/99999999"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -88,7 +90,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/rgbColors/" + existingColor.StringId; + string route = $"/rgbColors/{existingColor.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -123,7 +125,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/workItemGroups/" + existingGroup.StringId; + string route = $"/workItemGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -159,7 +161,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -195,7 +197,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs index 5bcb2f98a0..8e89e052d1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs @@ -42,11 +42,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("userAccounts"); - responseDocument.SingleData.Id.Should().Be(workItem.Assignee.StringId); - responseDocument.SingleData.Attributes.Should().BeNull(); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("userAccounts"); + responseDocument.Data.SingleValue.Id.Should().Be(workItem.Assignee.StringId); + responseDocument.Data.SingleValue.Attributes.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } [Fact] @@ -68,7 +68,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.Should().BeNull(); + responseDocument.Data.Value.Should().BeNull(); } [Fact] @@ -92,14 +92,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - ResourceObject item1 = responseDocument.ManyData.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(0).StringId); + ResourceObject item1 = responseDocument.Data.ManyValue.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(0).StringId); item1.Type.Should().Be("workItems"); item1.Attributes.Should().BeNull(); item1.Relationships.Should().BeNull(); - ResourceObject item2 = responseDocument.ManyData.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(1).StringId); + ResourceObject item2 = responseDocument.Data.ManyValue.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(1).StringId); item2.Type.Should().Be("workItems"); item2.Attributes.Should().BeNull(); item2.Relationships.Should().BeNull(); @@ -125,7 +125,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().BeEmpty(); + responseDocument.Data.ManyValue.Should().BeEmpty(); } [Fact] @@ -149,14 +149,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - ResourceObject item1 = responseDocument.ManyData.Single(resource => resource.Id == workItem.Tags.ElementAt(0).StringId); + ResourceObject item1 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItem.Tags.ElementAt(0).StringId); item1.Type.Should().Be("workTags"); item1.Attributes.Should().BeNull(); item1.Relationships.Should().BeNull(); - ResourceObject item2 = responseDocument.ManyData.Single(resource => resource.Id == workItem.Tags.ElementAt(1).StringId); + ResourceObject item2 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItem.Tags.ElementAt(1).StringId); item2.Type.Should().Be("workTags"); item2.Attributes.Should().BeNull(); item2.Relationships.Should().BeNull(); @@ -182,13 +182,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().BeEmpty(); + responseDocument.Data.ManyValue.Should().BeEmpty(); } [Fact] public async Task Cannot_get_relationship_for_unknown_primary_type() { - const string route = "/doesNotExist/99999999/relationships/assignee"; + string route = $"/{Unknown.ResourceType}/{Unknown.StringId.Int32}/relationships/assignee"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -202,20 +202,22 @@ public async Task Cannot_get_relationship_for_unknown_primary_type() [Fact] public async Task Cannot_get_relationship_for_unknown_primary_ID() { - const string route = "/workItems/99999999/relationships/assignee"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -229,20 +231,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = $"/workItems/{workItem.StringId}/relationships/doesNotExist"; + string route = $"/workItems/{workItem.StringId}/relationships/{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested relationship does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 9185b53bcb..fbfa714995 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -44,20 +45,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - ResourceObject item1 = responseDocument.ManyData.Single(resource => resource.Id == workItems[0].StringId); + ResourceObject item1 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItems[0].StringId); item1.Type.Should().Be("workItems"); item1.Attributes["description"].Should().Be(workItems[0].Description); - item1.Attributes["dueAt"].Should().BeCloseTo(workItems[0].DueAt); - item1.Attributes["priority"].Should().Be(workItems[0].Priority.ToString("G")); + item1.Attributes["dueAt"].As().Should().BeCloseTo(workItems[0].DueAt.GetValueOrDefault()); + item1.Attributes["priority"].Should().Be(workItems[0].Priority); item1.Relationships.Should().NotBeEmpty(); - ResourceObject item2 = responseDocument.ManyData.Single(resource => resource.Id == workItems[1].StringId); + ResourceObject item2 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItems[1].StringId); item2.Type.Should().Be("workItems"); item2.Attributes["description"].Should().Be(workItems[1].Description); - item2.Attributes["dueAt"].Should().BeCloseTo(workItems[1].DueAt); - item2.Attributes["priority"].Should().Be(workItems[1].Priority.ToString("G")); + item2.Attributes["dueAt"].As().Should().BeCloseTo(workItems[1].DueAt.GetValueOrDefault()); + item2.Attributes["priority"].Should().Be(workItems[1].Priority); item2.Relationships.Should().NotBeEmpty(); } @@ -65,7 +66,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_get_primary_resources_for_unknown_type() { // Arrange - const string route = "/doesNotExist"; + const string route = "/" + Unknown.ResourceType; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -88,7 +89,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/workItems/" + workItem.StringId; + string route = $"/workItems/{workItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -96,20 +97,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(workItem.StringId); - responseDocument.SingleData.Attributes["description"].Should().Be(workItem.Description); - responseDocument.SingleData.Attributes["dueAt"].Should().BeCloseTo(workItem.DueAt); - responseDocument.SingleData.Attributes["priority"].Should().Be(workItem.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(workItem.StringId); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(workItem.Description); + responseDocument.Data.SingleValue.Attributes["dueAt"].As().Should().BeCloseTo(workItem.DueAt.GetValueOrDefault()); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(workItem.Priority); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); } [Fact] public async Task Cannot_get_primary_resource_for_unknown_type() { // Arrange - const string route = "/doesNotExist/99999999"; + string route = $"/{Unknown.ResourceType}/{Unknown.StringId.Int32}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -124,20 +125,22 @@ public async Task Cannot_get_primary_resource_for_unknown_type() public async Task Cannot_get_primary_resource_for_unknown_ID() { // Arrange - const string route = "/workItems/99999999"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -161,12 +164,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("userAccounts"); - responseDocument.SingleData.Id.Should().Be(workItem.Assignee.StringId); - responseDocument.SingleData.Attributes["firstName"].Should().Be(workItem.Assignee.FirstName); - responseDocument.SingleData.Attributes["lastName"].Should().Be(workItem.Assignee.LastName); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("userAccounts"); + responseDocument.Data.SingleValue.Id.Should().Be(workItem.Assignee.StringId); + responseDocument.Data.SingleValue.Attributes["firstName"].Should().Be(workItem.Assignee.FirstName); + responseDocument.Data.SingleValue.Attributes["lastName"].Should().Be(workItem.Assignee.LastName); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); } [Fact] @@ -189,7 +192,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.Should().BeNull(); + responseDocument.Data.Value.Should().BeNull(); } [Fact] @@ -213,20 +216,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - ResourceObject item1 = responseDocument.ManyData.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(0).StringId); + ResourceObject item1 = responseDocument.Data.ManyValue.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(0).StringId); item1.Type.Should().Be("workItems"); item1.Attributes["description"].Should().Be(userAccount.AssignedItems.ElementAt(0).Description); - item1.Attributes["dueAt"].Should().BeCloseTo(userAccount.AssignedItems.ElementAt(0).DueAt); - item1.Attributes["priority"].Should().Be(userAccount.AssignedItems.ElementAt(0).Priority.ToString("G")); + item1.Attributes["dueAt"].As().Should().BeCloseTo(userAccount.AssignedItems.ElementAt(0).DueAt.GetValueOrDefault()); + item1.Attributes["priority"].Should().Be(userAccount.AssignedItems.ElementAt(0).Priority); item1.Relationships.Should().NotBeEmpty(); - ResourceObject item2 = responseDocument.ManyData.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(1).StringId); + ResourceObject item2 = responseDocument.Data.ManyValue.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(1).StringId); item2.Type.Should().Be("workItems"); item2.Attributes["description"].Should().Be(userAccount.AssignedItems.ElementAt(1).Description); - item2.Attributes["dueAt"].Should().BeCloseTo(userAccount.AssignedItems.ElementAt(1).DueAt); - item2.Attributes["priority"].Should().Be(userAccount.AssignedItems.ElementAt(1).Priority.ToString("G")); + item2.Attributes["dueAt"].As().Should().BeCloseTo(userAccount.AssignedItems.ElementAt(1).DueAt.GetValueOrDefault()); + item2.Attributes["priority"].Should().Be(userAccount.AssignedItems.ElementAt(1).Priority); item2.Relationships.Should().NotBeEmpty(); } @@ -250,7 +253,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().BeEmpty(); + responseDocument.Data.ManyValue.Should().BeEmpty(); } [Fact] @@ -274,15 +277,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - ResourceObject item1 = responseDocument.ManyData.Single(resource => resource.Id == workItem.Tags.ElementAt(0).StringId); + ResourceObject item1 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItem.Tags.ElementAt(0).StringId); item1.Type.Should().Be("workTags"); item1.Attributes["text"].Should().Be(workItem.Tags.ElementAt(0).Text); item1.Attributes["isBuiltIn"].Should().Be(workItem.Tags.ElementAt(0).IsBuiltIn); item1.Relationships.Should().NotBeEmpty(); - ResourceObject item2 = responseDocument.ManyData.Single(resource => resource.Id == workItem.Tags.ElementAt(1).StringId); + ResourceObject item2 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItem.Tags.ElementAt(1).StringId); item2.Type.Should().Be("workTags"); item2.Attributes["text"].Should().Be(workItem.Tags.ElementAt(1).Text); item2.Attributes["isBuiltIn"].Should().Be(workItem.Tags.ElementAt(1).IsBuiltIn); @@ -309,14 +312,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().BeEmpty(); + responseDocument.Data.ManyValue.Should().BeEmpty(); } [Fact] public async Task Cannot_get_secondary_resource_for_unknown_primary_type() { // Arrange - const string route = "/doesNotExist/99999999/assignee"; + string route = $"/{Unknown.ResourceType}/{Unknown.StringId.Int32}/assignee"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -331,20 +334,22 @@ public async Task Cannot_get_secondary_resource_for_unknown_primary_type() public async Task Cannot_get_secondary_resource_for_unknown_primary_ID() { // Arrange - const string route = "/workItems/99999999/assignee"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -360,20 +365,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = $"/workItems/{workItem.StringId}/doesNotExist"; + string route = $"/workItems/{workItem.StringId}/{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested relationship does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemDefinition.cs index 81f437dc85..bac9be6cc9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemDefinition.cs @@ -28,7 +28,7 @@ public override async Task OnWriteSucceededAsync(WorkItem resource, WriteOperati { if (writeOperation is not WriteOperationKind.DeleteResource) { - string statement = "Update \"WorkItems\" SET \"Description\" = '" + resource.Description + Suffix + "' WHERE \"Id\" = '" + resource.Id + "'"; + string statement = $"Update \"WorkItems\" SET \"Description\" = '{resource.Description}{Suffix}' WHERE \"Id\" = '{resource.StringId}'"; await _dbContext.Database.ExecuteSqlRawAsync(statement, cancellationToken); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemGroupDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemGroupDefinition.cs index 23afe8480b..a5762b761c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemGroupDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemGroupDefinition.cs @@ -29,7 +29,7 @@ public override async Task OnWriteSucceededAsync(WorkItemGroup resource, WriteOp { if (writeOperation is not WriteOperationKind.DeleteResource) { - string statement = "Update \"Groups\" SET \"Name\" = '" + resource.Name + Suffix + "' WHERE \"Id\" = '" + resource.Id + "'"; + string statement = $"Update \"Groups\" SET \"Name\" = '{resource.Name}{Suffix}' WHERE \"Id\" = '{resource.StringId}'"; await _dbContext.Database.ExecuteSqlRawAsync(statement, cancellationToken); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index d0b1e39b98..d66a425101 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -49,14 +49,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Only to-many relationships can be updated through this endpoint."); error.Detail.Should().Be("Relationship 'assignee' must be a to-many relationship."); @@ -193,14 +193,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -224,7 +224,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - id = 99999999 + id = Unknown.StringId.For() } } }; @@ -232,14 +232,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); @@ -263,8 +263,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } }; @@ -272,17 +272,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -311,14 +311,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Request body: <<"); @@ -336,6 +336,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId1 = Unknown.StringId.For(); + string userAccountId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new[] @@ -343,12 +346,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 88888888 + id = userAccountId1 }, new { type = "userAccounts", - id = 99999999 + id = userAccountId2 } } }; @@ -356,22 +359,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId1}' in relationship 'subscribers' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId2}' in relationship 'subscribers' does not exist."); } [Fact] @@ -386,6 +389,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string tagId1 = Unknown.StringId.For(); + string tagId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new[] @@ -393,12 +399,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "workTags", - id = 88888888 + id = tagId1 }, new { type = "workTags", - id = 99999999 + id = tagId2 } } }; @@ -406,22 +412,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId1}' in relationship 'tags' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId2}' in relationship 'tags' does not exist."); } [Fact] @@ -449,7 +455,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/doesNotExist/{existingWorkItem.StringId}/relationships/subscribers"; + string route = $"/{Unknown.ResourceType}/{existingWorkItem.StringId}/relationships/subscribers"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -484,20 +490,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - const string route = "/workItems/99999999/relationships/subscribers"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -519,25 +527,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 99999999 + id = Unknown.StringId.For() } } }; - string route = $"/workItems/{existingWorkItem.StringId}/relationships/doesNotExist"; + string route = $"/workItems/{existingWorkItem.StringId}/relationships/{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested relationship does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); } [Fact] @@ -568,14 +576,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); @@ -687,14 +695,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); @@ -720,14 +728,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index d227ab5e09..ed1f9755ab 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -64,14 +64,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Only to-many relationships can be updated through this endpoint."); error.Detail.Should().Be("Relationship 'assignee' must be a to-many relationship."); @@ -309,14 +309,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -349,14 +349,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); @@ -380,8 +380,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } }; @@ -389,17 +389,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -428,14 +428,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Request body: <<"); @@ -453,6 +453,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId1 = Unknown.StringId.For(); + string userAccountId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new[] @@ -460,12 +463,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 88888888 + id = userAccountId1 }, new { type = "userAccounts", - id = 99999999 + id = userAccountId2 } } }; @@ -473,22 +476,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId1}' in relationship 'subscribers' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId2}' in relationship 'subscribers' does not exist."); } [Fact] @@ -503,6 +506,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string tagId1 = Unknown.StringId.For(); + string tagId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new[] @@ -510,12 +516,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "workTags", - id = 88888888 + id = tagId1 }, new { type = "workTags", - id = 99999999 + id = tagId2 } } }; @@ -523,22 +529,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId1}' in relationship 'tags' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId2}' in relationship 'tags' does not exist."); } [Fact] @@ -566,7 +572,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/doesNotExist/{existingWorkItem.StringId}/relationships/subscribers"; + string route = $"/{Unknown.ResourceType}/{existingWorkItem.StringId}/relationships/subscribers"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); @@ -601,20 +607,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - const string route = "/workItems/99999999/relationships/subscribers"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -636,25 +644,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 99999999 + id = Unknown.StringId.For() } } }; - string route = $"/workItems/{existingWorkItem.StringId}/relationships/doesNotExist"; + string route = $"/workItems/{existingWorkItem.StringId}/relationships/{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested relationship does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); } [Fact] @@ -685,14 +693,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); @@ -806,14 +814,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); @@ -839,14 +847,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index 8ae734fc0f..3b439c1cec 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -225,14 +225,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -256,7 +256,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - id = 99999999 + id = Unknown.StringId.For() } } }; @@ -264,14 +264,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); @@ -295,8 +295,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } }; @@ -304,17 +304,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -343,14 +343,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Request body: <<"); @@ -368,6 +368,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId1 = Unknown.StringId.For(); + string userAccountId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new[] @@ -375,12 +378,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 88888888 + id = userAccountId1 }, new { type = "userAccounts", - id = 99999999 + id = userAccountId2 } } }; @@ -388,22 +391,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId1}' in relationship 'subscribers' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId2}' in relationship 'subscribers' does not exist."); } [Fact] @@ -418,6 +421,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string tagId1 = Unknown.StringId.For(); + string tagId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new[] @@ -425,12 +431,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "workTags", - id = 88888888 + id = tagId1 }, new { type = "workTags", - id = 99999999 + id = tagId2 } } }; @@ -438,22 +444,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId1}' in relationship 'tags' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId2}' in relationship 'tags' does not exist."); } [Fact] @@ -481,7 +487,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/doesNotExist/{existingWorkItem.StringId}/relationships/subscribers"; + string route = $"/{Unknown.ResourceType}/{existingWorkItem.StringId}/relationships/subscribers"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -509,20 +515,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = Array.Empty() }; - const string route = "/workItems/99999999/relationships/subscribers"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -544,25 +552,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 99999999 + id = Unknown.StringId.For() } } }; - string route = $"/workItems/{existingWorkItem.StringId}/relationships/doesNotExist"; + string route = $"/workItems/{existingWorkItem.StringId}/relationships/{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested relationship does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); } [Fact] @@ -593,14 +601,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); @@ -679,14 +687,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); @@ -712,14 +720,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index e12ebf1ea4..afe4fc3c97 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -232,9 +232,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - int itemId = existingUserAccounts[0].AssignedItems.ElementAt(1).Id; + int workItemId = existingUserAccounts[0].AssignedItems.ElementAt(1).Id; - WorkItem workItemInDatabase2 = await dbContext.WorkItems.Include(workItem => workItem.Assignee).FirstWithIdAsync(itemId); + WorkItem workItemInDatabase2 = await dbContext.WorkItems.Include(workItem => workItem.Assignee).FirstWithIdAsync(workItemId); workItemInDatabase2.Assignee.Should().NotBeNull(); workItemInDatabase2.Assignee.Id.Should().Be(existingUserAccounts[1].Id); @@ -258,14 +258,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -287,21 +287,21 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = 99999999 + id = Unknown.StringId.For() } }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); @@ -323,25 +323,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -367,14 +367,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Request body: <<"); @@ -392,29 +392,31 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId = Unknown.StringId.For(); + var requestBody = new { data = new { type = "userAccounts", - id = 99999999 + id = userAccountId } }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'assignee' does not exist."); + error.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId}' in relationship 'assignee' does not exist."); } [Fact] @@ -439,7 +441,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/doesNotExist/{existingWorkItem.StringId}/relationships/assignee"; + string route = $"/{Unknown.ResourceType}/{existingWorkItem.StringId}/relationships/assignee"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -471,20 +473,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - const string route = "/workItems/99999999/relationships/assignee"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -504,24 +508,24 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "userAccounts", - id = 99999999 + id = Unknown.StringId.For() } }; - string route = $"/workItems/{existingWorkItem.StringId}/relationships/doesNotExist"; + string route = $"/workItems/{existingWorkItem.StringId}/relationships/{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested relationship does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); } [Fact] @@ -549,14 +553,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); @@ -592,14 +596,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().StartWith("Expected single data element for 'assignee' relationship. - Request body: <<"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index 19d0979648..b225589c4e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -60,7 +60,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -68,7 +68,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -107,7 +107,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -115,7 +115,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -168,7 +168,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -176,7 +176,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -237,7 +237,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -245,7 +245,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -302,11 +302,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(existingWorkItem.Priority); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); @@ -368,14 +368,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["tags"].ManyData.Should().HaveCount(1); - responseDocument.SingleData.Relationships["tags"].ManyData[0].Id.Should().Be(existingTag.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(existingWorkItem.Priority); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue[0].Id.Should().Be(existingTag.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("workTags"); @@ -384,7 +384,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Attributes["text"].Should().Be(existingTag.Text); responseDocument.Included[0].Relationships.Should().BeNull(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -421,7 +421,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - id = 99999999 + id = Unknown.StringId.For() } } } @@ -429,17 +429,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'subscribers' relationship. - Request body: <<"); @@ -471,8 +471,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -480,20 +480,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -530,17 +530,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Expected 'id' element in 'subscribers' relationship. - Request body: <<"); @@ -558,6 +558,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId1 = Unknown.StringId.For(); + string userAccountId2 = Unknown.StringId.AltFor(); + + string tagId1 = Unknown.StringId.For(); + string tagId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new @@ -573,12 +579,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 88888888 + id = userAccountId1 }, new { type = "userAccounts", - id = 99999999 + id = userAccountId2 } } }, @@ -589,12 +595,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "workTags", - id = 88888888 + id = tagId1 }, new { type = "workTags", - id = 99999999 + id = tagId2 } } } @@ -602,35 +608,35 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(4); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId1}' in relationship 'subscribers' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId2}' in relationship 'subscribers' does not exist."); - Error error3 = responseDocument.Errors[2]; + ErrorObject error3 = responseDocument.Errors[2]; error3.StatusCode.Should().Be(HttpStatusCode.NotFound); error3.Title.Should().Be("A related resource does not exist."); - error3.Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); + error3.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId1}' in relationship 'tags' does not exist."); - Error error4 = responseDocument.Errors[3]; + ErrorObject error4 = responseDocument.Errors[3]; error4.StatusCode.Should().Be(HttpStatusCode.NotFound); error4.Title.Should().Be("A related resource does not exist."); - error4.Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); + error4.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId2}' in relationship 'tags' does not exist."); } [Fact] @@ -668,17 +674,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().StartWith("Relationship 'subscribers' contains incompatible resource type 'rgbColors'. - Request body: <<"); @@ -727,7 +733,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -735,7 +741,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -774,17 +780,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); @@ -818,17 +824,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); @@ -865,7 +871,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -873,7 +879,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -914,7 +920,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -922,7 +928,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -977,7 +983,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -985,7 +991,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1031,7 +1037,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -1039,7 +1045,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index aea4a3734b..a3bf1986f2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -63,7 +63,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/userAccounts/" + existingUserAccount.StringId; + string route = $"/userAccounts/{existingUserAccount.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -109,7 +109,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/userAccounts/" + existingUserAccount.StringId; + string route = $"/userAccounts/{existingUserAccount.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -152,15 +152,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 } } } } }; - string route = "/userAccounts/" + existingUserAccount.StringId; + string route = $"/userAccounts/{existingUserAccount.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -197,7 +197,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItemGroups/" + existingGroup.StringId; + string route = $"/workItemGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -205,18 +205,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItemGroups"); - responseDocument.SingleData.Id.Should().Be(existingGroup.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(newName + ImplicitlyChangingWorkItemGroupDefinition.Suffix); - responseDocument.SingleData.Attributes["isPublic"].Should().Be(existingGroup.IsPublic); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItemGroups"); + responseDocument.Data.SingleValue.Id.Should().Be(existingGroup.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be($"{newName}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"); + responseDocument.Data.SingleValue.Attributes["isPublic"].Should().Be(existingGroup.IsPublic); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { WorkItemGroup groupInDatabase = await dbContext.Groups.FirstWithIdAsync(existingGroup.Id); - groupInDatabase.Name.Should().Be(newName + ImplicitlyChangingWorkItemGroupDefinition.Suffix); + groupInDatabase.Name.Should().Be($"{newName}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"); groupInDatabase.IsPublic.Should().Be(existingGroup.IsPublic); }); @@ -250,7 +250,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/rgbColors/" + existingColor.StringId; + string route = $"/rgbColors/{existingColor.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -298,7 +298,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/userAccounts/" + existingUserAccount.StringId; + string route = $"/userAccounts/{existingUserAccount.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -344,7 +344,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -352,20 +352,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes["description"].Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); - responseDocument.SingleData.Attributes["dueAt"].Should().BeNull(); - responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); - responseDocument.SingleData.Attributes["isImportant"].Should().Be(existingWorkItem.IsImportant); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); + responseDocument.Data.SingleValue.Attributes["dueAt"].Should().BeNull(); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(existingWorkItem.Priority); + responseDocument.Data.SingleValue.Attributes["isImportant"].Should().Be(existingWorkItem.IsImportant); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { WorkItem workItemInDatabase = await dbContext.WorkItems.FirstWithIdAsync(existingWorkItem.Id); - workItemInDatabase.Description.Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); + workItemInDatabase.Description.Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); workItemInDatabase.DueAt.Should().BeNull(); workItemInDatabase.Priority.Should().Be(existingWorkItem.Priority); }); @@ -406,19 +406,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(2); - responseDocument.SingleData.Attributes["description"].Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); - responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(2); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(existingWorkItem.Priority); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { WorkItem workItemInDatabase = await dbContext.WorkItems.FirstWithIdAsync(existingWorkItem.Id); - workItemInDatabase.Description.Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); + workItemInDatabase.Description.Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); workItemInDatabase.DueAt.Should().BeNull(); workItemInDatabase.Priority.Should().Be(existingWorkItem.Priority); }); @@ -461,15 +461,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(2); - responseDocument.SingleData.Attributes["description"].Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); - responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["tags"].ManyData.Should().HaveCount(1); - responseDocument.SingleData.Relationships["tags"].ManyData[0].Id.Should().Be(existingWorkItem.Tags.Single().StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(2); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(existingWorkItem.Priority); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue[0].Id.Should().Be(existingWorkItem.Tags.Single().StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("workTags"); @@ -482,7 +482,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { WorkItem workItemInDatabase = await dbContext.WorkItems.FirstWithIdAsync(existingWorkItem.Id); - workItemInDatabase.Description.Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); + workItemInDatabase.Description.Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); workItemInDatabase.DueAt.Should().BeNull(); workItemInDatabase.Priority.Should().Be(existingWorkItem.Priority); }); @@ -510,7 +510,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -518,9 +518,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Values.Should().OnlyContain(relationshipEntry => relationshipEntry.Data == null); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Values.Should().OnlyContain(relationshipObject => relationshipObject.Data.Value == null); responseDocument.Included.Should().BeNull(); } @@ -539,17 +539,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string requestBody = string.Empty; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -575,17 +575,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); @@ -607,25 +607,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "doesNotExist", + type = Unknown.ResourceType, id = existingWorkItem.StringId } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -648,17 +648,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Request body: <<"); @@ -685,7 +685,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/doesNotExist/" + existingWorkItem.StringId; + string route = $"/{Unknown.ResourceType}/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -700,29 +700,31 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() { // Arrange + string workItemId = Unknown.StringId.For(); + var requestBody = new { data = new { type = "workItems", - id = 99999999 + id = workItemId } }; - const string route = "/workItems/99999999"; + string route = $"/workItems/{workItemId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -746,17 +748,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); @@ -785,17 +787,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItems[1].StringId; + string route = $"/workItems/{existingWorkItems[1].StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource ID mismatch between request body and endpoint URL."); @@ -828,20 +830,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Changing the value of the requested attribute is not allowed."); - error.Detail.Should().StartWith("Changing the value of 'isImportant' is not allowed. - Request body:"); + error.Detail.Should().StartWith("Changing the value of 'isImportant' is not allowed. - Request body: <<"); } [Fact] @@ -869,20 +871,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItemGroups/" + existingWorkItem.StringId; + string route = $"/workItemGroups/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); - error.Detail.Should().StartWith("Attribute 'isDeprecated' is read-only. - Request body:"); + error.Detail.Should().StartWith("Attribute 'isDeprecated' is read-only. - Request body: <<"); } [Fact] @@ -897,22 +899,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - const string requestBody = "{ \"data\" {"; + const string requestBody = "{ \"data {"; - string route = "/workItemGroups/" + existingWorkItem.StringId; + string route = $"/workItemGroups/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Invalid character after parsing"); + error.Detail.Should().Match("Expected end of string, but instead reached end of data. * - Request body: <<*"); } [Fact] @@ -935,27 +937,67 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingWorkItem.StringId, attributes = new { - id = existingWorkItem.Id + 123456 + id = Unknown.StringId.For() } } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); + error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("Resource ID is read-only. - Request body: <<"); } + [Fact] + public async Task Cannot_update_resource_with_incompatible_ID_value() + { + // Arrange + WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.WorkItems.Add(existingWorkItem); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "workItems", + id = existingWorkItem.Id, + attributes = new + { + } + } + }; + + string route = $"/workItems/{existingWorkItem.StringId}"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + + ErrorObject error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().StartWith($"Failed to convert ID '{existingWorkItem.Id}' of type 'Number' to type 'String'. - Request body: <<"); + } + [Fact] public async Task Cannot_update_resource_with_incompatible_attribute_value() { @@ -976,25 +1018,31 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingWorkItem.StringId, attributes = new { - dueAt = "not-a-valid-time" + dueAt = new + { + Start = 10, + End = 20 + } } } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'Nullable`1'. - Request body: <<"); + + error.Detail.Should().Match("Failed to convert attribute 'dueAt' with value '*start*end*' " + + "of type 'Object' to type 'Nullable'. - Request body: <<*"); } [Fact] @@ -1064,7 +1112,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -1072,9 +1120,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["description"].Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1090,7 +1138,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - workItemInDatabase.Description.Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); + workItemInDatabase.Description.Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); workItemInDatabase.Assignee.Should().NotBeNull(); workItemInDatabase.Assignee.Id.Should().Be(existingUserAccounts[0].Id); @@ -1160,7 +1208,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -1168,7 +1216,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index ded40750a3..3eac83cc04 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -60,7 +60,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -68,7 +68,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -113,7 +113,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItemGroups/" + existingGroup.StringId; + string route = $"/workItemGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -169,7 +169,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/rgbColors/" + existingGroups[0].Color.StringId; + string route = $"/rgbColors/{existingGroups[0].Color.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -231,7 +231,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/rgbColors/" + existingColor.StringId; + string route = $"/rgbColors/{existingColor.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -282,20 +282,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingUserAccounts[0].AssignedItems.ElementAt(1).StringId; + string route = $"/workItems/{existingUserAccounts[0].AssignedItems.ElementAt(1).StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { - int itemId = existingUserAccounts[0].AssignedItems.ElementAt(1).Id; + int workItemId = existingUserAccounts[0].AssignedItems.ElementAt(1).Id; - WorkItem workItemInDatabase2 = await dbContext.WorkItems.Include(workItem => workItem.Assignee).FirstWithIdAsync(itemId); + WorkItem workItemInDatabase2 = await dbContext.WorkItems.Include(workItem => workItem.Assignee).FirstWithIdAsync(workItemId); workItemInDatabase2.Assignee.Should().NotBeNull(); workItemInDatabase2.Assignee.Id.Should().Be(existingUserAccounts[1].Id); @@ -343,11 +343,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes["description"].Should().Be(existingWorkItem.Description + ImplicitlyChangingWorkItemDefinition.Suffix); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + string description = $"{existingWorkItem.Description}{ImplicitlyChangingWorkItemDefinition.Suffix}"; + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(description); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); @@ -408,13 +410,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["description"].Should().Be(existingWorkItem.Description + ImplicitlyChangingWorkItemDefinition.Suffix); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["assignee"].SingleData.Id.Should().Be(existingUserAccount.StringId); + string description = $"{existingWorkItem.Description}{ImplicitlyChangingWorkItemDefinition.Suffix}"; + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(description); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["assignee"].Data.SingleValue.Id.Should().Be(existingUserAccount.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); @@ -456,24 +460,24 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = 99999999 + id = Unknown.StringId.For() } } } } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'assignee' relationship. - Request body: <<"); @@ -503,28 +507,28 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -558,17 +562,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Expected 'id' element in 'assignee' relationship. - Request body: <<"); @@ -586,6 +590,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId = Unknown.StringId.For(); + var requestBody = new { data = new @@ -599,27 +605,27 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "userAccounts", - id = 99999999 + id = userAccountId } } } } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'assignee' does not exist."); + error.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId}' in relationship 'assignee' does not exist."); } [Fact] @@ -654,17 +660,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().StartWith("Relationship 'assignee' contains incompatible resource type 'rgbColors'. - Request body: <<"); @@ -706,17 +712,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().StartWith("Expected single data element for 'assignee' relationship. - Request body: <<"); @@ -753,7 +759,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -761,7 +767,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -803,7 +809,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -811,7 +817,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/WorkItemPriority.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/WorkItemPriority.cs index 093a2d5a86..e7a5a113bf 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/WorkItemPriority.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/WorkItemPriority.cs @@ -1,8 +1,10 @@ +using System.Text.Json.Serialization; using JetBrains.Annotations; namespace JsonApiDotNetCoreTests.IntegrationTests.ReadWrite { [UsedImplicitly(ImplicitUseTargetFlags.Members)] + [JsonConverter(typeof(JsonStringEnumMemberConverter))] public enum WorkItemPriority { Low, diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs index 140065daed..20448ea5db 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs @@ -51,14 +51,14 @@ public async Task Cannot_create_dependent_side_of_required_ManyToOne_relationshi const string route = "/orders"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); error.Title.Should().Be("An unhandled error occurred while processing this request."); error.Detail.Should().Be("Failed to persist changes in the underlying data store."); @@ -85,14 +85,14 @@ public async Task Cannot_create_dependent_side_of_required_OneToOne_relationship const string route = "/shipments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); error.Title.Should().Be("An unhandled error occurred while processing this request."); error.Detail.Should().Be("Failed to persist changes in the underlying data store."); @@ -111,7 +111,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = $"/customers/{existingOrder.Customer.Id}"; + string route = $"/customers/{existingOrder.Customer.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -145,7 +145,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = $"/orders/{existingOrder.Id}"; + string route = $"/orders/{existingOrder.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -186,7 +186,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = existingOrder.Id, + id = existingOrder.StringId, type = "orders", relationships = new { @@ -198,17 +198,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/orders/{existingOrder.Id}"; + string route = $"/orders/{existingOrder.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); @@ -235,17 +235,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = (object)null }; - string route = $"/orders/{existingOrder.Id}/relationships/customer"; + string route = $"/orders/{existingOrder.StringId}/relationships/customer"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); @@ -271,7 +271,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = existingOrder.Customer.Id, + id = existingOrder.Customer.StringId, type = "customers", relationships = new { @@ -283,17 +283,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/customers/{existingOrder.Customer.Id}"; + string route = $"/customers/{existingOrder.Customer.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); @@ -320,17 +320,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = Array.Empty() }; - string route = $"/customers/{existingOrder.Customer.Id}/relationships/orders"; + string route = $"/customers/{existingOrder.Customer.StringId}/relationships/orders"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); @@ -359,22 +359,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "orders", - id = existingOrder.Id + id = existingOrder.StringId } } }; - string route = $"/customers/{existingOrder.Customer.Id}/relationships/orders"; + string route = $"/customers/{existingOrder.Customer.StringId}/relationships/orders"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); @@ -403,7 +403,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = orderWithoutShipment.Id, + id = orderWithoutShipment.StringId, type = "orders", relationships = new { @@ -411,7 +411,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = orderWithShipment.Shipment.Id, + id = orderWithShipment.Shipment.StringId, type = "shipments" } } @@ -419,7 +419,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/orders/{orderWithoutShipment.Id}"; + string route = $"/orders/{orderWithoutShipment.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -459,12 +459,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = orderWithShipment.Shipment.Id, + id = orderWithShipment.Shipment.StringId, type = "shipments" } }; - string route = $"/orders/{orderWithoutShipment.Id}/relationships/shipment"; + string route = $"/orders/{orderWithoutShipment.StringId}/relationships/shipment"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceConstructorInjection/ResourceInjectionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceConstructorInjection/ResourceInjectionTests.cs index c08840a5d6..97716b7812 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceConstructorInjection/ResourceInjectionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceConstructorInjection/ResourceInjectionTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Net; using System.Net.Http; @@ -50,7 +51,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/giftCertificates/" + certificate.StringId; + string route = $"/giftCertificates/{certificate.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -58,10 +59,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(certificate.StringId); - responseDocument.SingleData.Attributes["issueDate"].Should().BeCloseTo(certificate.IssueDate); - responseDocument.SingleData.Attributes["hasExpired"].Should().Be(false); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(certificate.StringId); + responseDocument.Data.SingleValue.Attributes["issueDate"].As().Should().BeCloseTo(certificate.IssueDate); + responseDocument.Data.SingleValue.Attributes["hasExpired"].Should().Be(false); } [Fact] @@ -88,10 +89,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(postOffices[1].StringId); - responseDocument.ManyData[0].Attributes["address"].Should().Be(postOffices[1].Address); - responseDocument.ManyData[0].Attributes["isOpen"].Should().Be(true); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(postOffices[1].StringId); + responseDocument.Data.ManyValue[0].Attributes["address"].Should().Be(postOffices[1].Address); + responseDocument.Data.ManyValue[0].Attributes["isOpen"].Should().Be(true); } [Fact] @@ -118,10 +119,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(certificate.Issuer.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["isOpen"].Should().Be(true); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(certificate.Issuer.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["isOpen"].Should().Be(true); } [Fact] @@ -172,17 +173,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["issueDate"].Should().BeCloseTo(newIssueDate); - responseDocument.SingleData.Attributes["hasExpired"].Should().Be(true); - responseDocument.SingleData.Relationships["issuer"].SingleData.Id.Should().Be(existingOffice.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["issueDate"].As().Should().BeCloseTo(newIssueDate); + responseDocument.Data.SingleValue.Attributes["hasExpired"].Should().Be(true); + responseDocument.Data.SingleValue.Relationships["issuer"].Data.SingleValue.Id.Should().Be(existingOffice.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(existingOffice.StringId); responseDocument.Included[0].Attributes["address"].Should().Be(existingOffice.Address); responseDocument.Included[0].Attributes["isOpen"].Should().Be(false); - int newCertificateId = int.Parse(responseDocument.SingleData.Id); + int newCertificateId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -241,7 +242,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/postOffices/" + existingOffice.StringId; + string route = $"/postOffices/{existingOffice.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -274,7 +275,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/postOffices/" + existingOffice.StringId; + string route = $"/postOffices/{existingOffice.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -296,20 +297,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_delete_unknown_resource() { // Arrange - const string route = "/postOffices/99999999"; + string officeId = Unknown.StringId.For(); + + string route = $"/postOffices/{officeId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'postOffices' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'postOffices' with ID '{officeId}' does not exist."); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs index 5cdf8d49fd..a40696a23a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs @@ -35,8 +35,7 @@ public override IImmutableList OnApplyIncludes(IImmuta return existingIncludes; } - RelationshipAttribute orbitsAroundRelationship = - ResourceContext.Relationships.Single(relationship => relationship.Property.Name == nameof(Moon.OrbitsAround)); + RelationshipAttribute orbitsAroundRelationship = ResourceContext.GetRelationshipByPropertyName(nameof(Moon.OrbitsAround)); return existingIncludes.Add(new IncludeElementExpression(orbitsAroundRelationship)); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs index e121087275..b2313bee98 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs @@ -34,7 +34,7 @@ public override IImmutableList OnApplyIncludes(IImmuta if (_clientSettingsProvider.IsIncludePlanetMoonsBlocked && existingIncludes.Any(include => include.Relationship.Property.Name == nameof(Planet.Moons))) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Including moons is not permitted." }); @@ -49,7 +49,7 @@ public override FilterExpression OnApplyFilter(FilterExpression existingFilter) if (_clientSettingsProvider.ArePlanetsWithPrivateNameHidden) { - AttrAttribute privateNameAttribute = ResourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Planet.PrivateName)); + AttrAttribute privateNameAttribute = ResourceContext.GetAttributeByPropertyName(nameof(Planet.PrivateName)); FilterExpression hasNoPrivateName = new ComparisonExpression(ComparisonOperator.Equals, new ResourceFieldChainExpression(privateNameAttribute), new NullConstantExpression()); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs index 0cd6d121d4..675d562f5f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -66,14 +67,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/planets?include=moons"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Including moons is not permitted."); error.Detail.Should().BeNull(); @@ -104,7 +105,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/moons/" + moon.StringId; + string route = $"/moons/{moon.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -112,9 +113,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Relationships["orbitsAround"].SingleData.Type.Should().Be("planets"); - responseDocument.SingleData.Relationships["orbitsAround"].SingleData.Id.Should().Be(moon.OrbitsAround.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["orbitsAround"].Data.SingleValue.Type.Should().Be("planets"); + responseDocument.Data.SingleValue.Relationships["orbitsAround"].Data.SingleValue.Id.Should().Be(moon.OrbitsAround.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("planets"); @@ -155,11 +156,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(planets[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(planets[3].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(planets[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(planets[3].StringId); - responseDocument.Meta["totalResources"].Should().Be(2); + ((JsonElement)responseDocument.Meta["total"]).GetInt32().Should().Be(2); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -205,10 +206,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(planets[3].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(planets[3].StringId); - responseDocument.Meta["totalResources"].Should().Be(1); + ((JsonElement)responseDocument.Meta["total"]).GetInt32().Should().Be(1); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -250,10 +251,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(stars[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(stars[0].StringId); - responseDocument.ManyData[2].Id.Should().Be(stars[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Id.Should().Be(stars[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(stars[0].StringId); + responseDocument.Data.ManyValue[2].Id.Should().Be(stars[2].StringId); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -297,10 +298,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(stars[2].StringId); - responseDocument.ManyData[1].Id.Should().Be(stars[0].StringId); - responseDocument.ManyData[2].Id.Should().Be(stars[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Id.Should().Be(stars[2].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(stars[0].StringId); + responseDocument.Data.ManyValue[2].Id.Should().Be(stars[1].StringId); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -335,7 +336,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(5); + responseDocument.Data.ManyValue.Should().HaveCount(5); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -369,11 +370,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(star.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(star.Name); - responseDocument.SingleData.Attributes["kind"].Should().Be(star.Kind.ToString()); - responseDocument.SingleData.Relationships.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(star.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(star.Name); + responseDocument.Data.SingleValue.Attributes["kind"].Should().Be(star.Kind); + responseDocument.Data.SingleValue.Relationships.Should().NotBeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -407,12 +408,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(star.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(2); - responseDocument.SingleData.Attributes["name"].Should().Be(star.Name); - responseDocument.SingleData.Attributes["solarRadius"].As().Should().BeApproximately(star.SolarRadius); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(star.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(2); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(star.Name); + responseDocument.Data.SingleValue.Attributes["solarRadius"].As().Should().BeApproximately(star.SolarRadius); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -446,11 +447,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(star.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(star.Name); - responseDocument.SingleData.Attributes.Should().NotContainKey("isVisibleFromEarth"); - responseDocument.SingleData.Relationships.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(star.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(star.Name); + responseDocument.Data.SingleValue.Attributes.Should().NotContainKey("isVisibleFromEarth"); + responseDocument.Data.SingleValue.Relationships.Should().NotBeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -484,11 +485,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(star.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["name"].Should().Be(star.Name); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(star.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(star.Name); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -526,8 +527,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(moons[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(moons[1].StringId); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -572,8 +573,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(moons[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(moons[2].StringId); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -601,14 +602,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/planets/{planet.StringId}/moons?isLargerThanTheSun=false"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Custom query string parameters cannot be used on nested resource endpoints."); error.Detail.Should().Be("Query string parameter 'isLargerThanTheSun' cannot be used on a nested resource endpoint."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/StarKind.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/StarKind.cs index 7a04056716..d3c4685d1c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/StarKind.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/StarKind.cs @@ -1,8 +1,10 @@ +using System.Text.Json.Serialization; using JetBrains.Annotations; namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceDefinitions.Reading { [UsedImplicitly(ImplicitUseTargetFlags.Members)] + [JsonConverter(typeof(JsonStringEnumMemberConverter))] public enum StarKind { Other, diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Serialization/ResourceDefinitionSerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Serialization/ResourceDefinitionSerializationTests.cs index 693907a79a..216de1a998 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Serialization/ResourceDefinitionSerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Serialization/ResourceDefinitionSerializationTests.cs @@ -63,12 +63,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - string socialSecurityNumber1 = encryptionService.Decrypt((string)responseDocument.ManyData[0].Attributes["socialSecurityNumber"]); + string socialSecurityNumber1 = encryptionService.Decrypt((string)responseDocument.Data.ManyValue[0].Attributes["socialSecurityNumber"]); socialSecurityNumber1.Should().Be(students[0].SocialSecurityNumber); - string socialSecurityNumber2 = encryptionService.Decrypt((string)responseDocument.ManyData[1].Attributes["socialSecurityNumber"]); + string socialSecurityNumber2 = encryptionService.Decrypt((string)responseDocument.Data.ManyValue[1].Attributes["socialSecurityNumber"]); socialSecurityNumber2.Should().Be(students[1].SocialSecurityNumber); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -104,7 +104,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); responseDocument.Included.Should().HaveCount(4); @@ -144,7 +144,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/students/" + student.StringId; + string route = $"/students/{student.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -152,9 +152,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); - string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.SingleData.Attributes["socialSecurityNumber"]); + string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.Data.SingleValue.Attributes["socialSecurityNumber"]); socialSecurityNumber.Should().Be(student.SocialSecurityNumber); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -187,12 +187,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - string socialSecurityNumber1 = encryptionService.Decrypt((string)responseDocument.ManyData[0].Attributes["socialSecurityNumber"]); + string socialSecurityNumber1 = encryptionService.Decrypt((string)responseDocument.Data.ManyValue[0].Attributes["socialSecurityNumber"]); socialSecurityNumber1.Should().Be(scholarship.Participants[0].SocialSecurityNumber); - string socialSecurityNumber2 = encryptionService.Decrypt((string)responseDocument.ManyData[1].Attributes["socialSecurityNumber"]); + string socialSecurityNumber2 = encryptionService.Decrypt((string)responseDocument.Data.ManyValue[1].Attributes["socialSecurityNumber"]); socialSecurityNumber2.Should().Be(scholarship.Participants[1].SocialSecurityNumber); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -226,9 +226,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); - string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.SingleData.Attributes["socialSecurityNumber"]); + string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.Data.SingleValue.Attributes["socialSecurityNumber"]); socialSecurityNumber.Should().Be(scholarship.PrimaryContact.SocialSecurityNumber); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -261,7 +261,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); @@ -305,12 +305,12 @@ public async Task Decrypts_on_create_resource() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); - string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.SingleData.Attributes["socialSecurityNumber"]); + string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.Data.SingleValue.Attributes["socialSecurityNumber"]); socialSecurityNumber.Should().Be(newSocialSecurityNumber); - int newStudentId = int.Parse(responseDocument.SingleData.Id); + int newStudentId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -376,7 +376,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); @@ -419,7 +419,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/students/" + existingStudent.StringId; + string route = $"/students/{existingStudent.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -427,9 +427,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); - string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.SingleData.Attributes["socialSecurityNumber"]); + string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.Data.SingleValue.Attributes["socialSecurityNumber"]); socialSecurityNumber.Should().Be(newSocialSecurityNumber); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -504,7 +504,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(2); @@ -544,8 +544,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(scholarship.PrimaryContact.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(scholarship.PrimaryContact.StringId); hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } @@ -573,9 +573,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(scholarship.Participants[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(scholarship.Participants[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(scholarship.Participants[0].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(scholarship.Participants[1].StringId); hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/InheritanceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/InheritanceTests.cs index be72317118..477b450167 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/InheritanceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/InheritanceTests.cs @@ -54,13 +54,13 @@ public async Task Can_create_resource_with_inherited_attributes() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("men"); - responseDocument.SingleData.Attributes["familyName"].Should().Be(newMan.FamilyName); - responseDocument.SingleData.Attributes["isRetired"].Should().Be(newMan.IsRetired); - responseDocument.SingleData.Attributes["hasBeard"].Should().Be(newMan.HasBeard); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("men"); + responseDocument.Data.SingleValue.Attributes["familyName"].Should().Be(newMan.FamilyName); + responseDocument.Data.SingleValue.Attributes["isRetired"].Should().Be(newMan.IsRetired); + responseDocument.Data.SingleValue.Attributes["hasBeard"].Should().Be(newMan.HasBeard); - int newManId = int.Parse(responseDocument.SingleData.Id); + int newManId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -112,8 +112,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - int newManId = int.Parse(responseDocument.SingleData.Id); + responseDocument.Data.SingleValue.Should().NotBeNull(); + int newManId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -163,7 +163,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/men/" + existingMan.StringId; + string route = $"/men/{existingMan.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -274,8 +274,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - int newManId = int.Parse(responseDocument.SingleData.Id); + responseDocument.Data.SingleValue.Should().NotBeNull(); + int newManId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -388,8 +388,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - int newManId = int.Parse(responseDocument.SingleData.Id); + responseDocument.Data.SingleValue.Should().NotBeNull(); + int newManId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/DisableQueryStringTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/DisableQueryStringTests.cs index e99ee891f8..54356a48f7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/DisableQueryStringTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/DisableQueryStringTests.cs @@ -35,14 +35,14 @@ public async Task Cannot_sort_if_query_string_parameter_is_blocked_by_controller const string route = "/sofas?sort=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'sort' cannot be used at this endpoint."); @@ -56,14 +56,14 @@ public async Task Cannot_paginate_if_query_string_parameter_is_blocked_by_contro const string route = "/sofas?page[number]=2"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'page[number]' cannot be used at this endpoint."); @@ -77,14 +77,14 @@ public async Task Cannot_use_custom_query_string_parameter_if_blocked_by_control const string route = "/beds?skipCache=true"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'skipCache' cannot be used at this endpoint."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs index b8894c852e..8a307b1de7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs @@ -51,14 +51,14 @@ public async Task Cannot_create_resource() const string route = "/beds"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); error.Detail.Should().Be("Endpoint does not support POST requests."); @@ -88,17 +88,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/beds/" + existingBed.StringId; + string route = $"/beds/{existingBed.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); error.Detail.Should().Be("Endpoint does not support PATCH requests."); @@ -116,17 +116,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/beds/" + existingBed.StringId; + string route = $"/beds/{existingBed.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); error.Detail.Should().Be("Endpoint does not support DELETE requests."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs index 88917b3fd8..fb7ceadc45 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs @@ -81,7 +81,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/sofas/" + existingSofa.StringId; + string route = $"/sofas/{existingSofa.StringId}"; // Act (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -102,17 +102,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/sofas/" + existingSofa.StringId; + string route = $"/sofas/{existingSofa.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); error.Detail.Should().Be("Endpoint does not support DELETE requests."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs index 1fbcbedade..5be0254212 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs @@ -81,17 +81,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/chairs/" + existingChair.StringId; + string route = $"/chairs/{existingChair.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); error.Detail.Should().Be("Endpoint does not support PATCH requests."); @@ -109,7 +109,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/chairs/" + existingChair.StringId; + string route = $"/chairs/{existingChair.StringId}"; // Act (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs index b9dde5217c..a6f16a1cb0 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs @@ -51,14 +51,14 @@ public async Task Cannot_create_resource() const string route = "/tables"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); error.Detail.Should().Be("Endpoint does not support POST requests."); @@ -88,7 +88,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/tables/" + existingTable.StringId; + string route = $"/tables/{existingTable.StringId}"; // Act (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -109,7 +109,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/tables/" + existingTable.StringId; + string route = $"/tables/{existingTable.StringId}"; // Act (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/ETagTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/ETagTests.cs index ebf88cda99..8763390bd9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/ETagTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/ETagTests.cs @@ -84,7 +84,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Returns_no_ETag_for_failed_GET_request() { // Arrange - const string route = "/meetings/99999999"; + string route = $"/meetings/{Unknown.StringId.For()}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -155,7 +155,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/meetings/" + existingMeeting.StringId; + string route = $"/meetings/{existingMeeting.StringId}"; Action setRequestHeaders = headers => { @@ -163,18 +163,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePatchAsync(route, requestBody, setRequestHeaders: setRequestHeaders); + (HttpResponseMessage httpResponse, Document responseDocument) = + await _testContext.ExecutePatchAsync(route, requestBody, setRequestHeaders: setRequestHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.PreconditionFailed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.PreconditionFailed); error.Title.Should().Be("Detection of mid-air edit collisions using ETags is not supported."); error.Detail.Should().BeNull(); + error.Source.Header.Should().Be("If-Match"); } [Fact] @@ -198,7 +199,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => Action setRequestHeaders2 = headers => { - headers.IfNoneMatch.ParseAdd("\"12345\", W/\"67890\", " + responseETag); + headers.IfNoneMatch.ParseAdd($"\"12345\", W/\"67890\", {responseETag}"); }; // Act diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/MeetingLocation.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/MeetingLocation.cs index 0d48615e25..8ca431bc1c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/MeetingLocation.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/MeetingLocation.cs @@ -1,13 +1,15 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCoreTests.IntegrationTests.Serialization { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class MeetingLocation { - [JsonProperty("lat")] + [JsonPropertyName("lat")] public double Latitude { get; set; } - [JsonProperty("lng")] + [JsonPropertyName("lng")] public double Longitude { get; set; } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs index 95524efea0..f73e03004d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs @@ -4,13 +4,12 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json.Serialization; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using TestBuildingBlocks; using Xunit; @@ -18,6 +17,8 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.Serialization { public sealed class SerializationTests : IClassFixture, SerializationDbContext>> { + private const string JsonDateTimeOffsetFormatSpecifier = "yyyy-MM-ddTHH:mm:ss.FFFFFFFK"; + private readonly IntegrationTestContext, SerializationDbContext> _testContext; private readonly SerializationFakers _fakers = new(); @@ -37,6 +38,11 @@ public SerializationTests(IntegrationTestContext converter is JsonTimeSpanConverter)) + { + options.SerializerOptions.Converters.Add(new JsonTimeSpanConverter()); + } } [Fact] @@ -51,7 +57,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/meetings/" + meeting.StringId; + string route = $"/meetings/{meeting.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteHeadAsync(route); @@ -66,7 +72,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Returns_no_body_for_failed_HEAD_request() { // Arrange - const string route = "/meetings/99999999"; + string route = $"/meetings/{Unknown.StringId.For()}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteHeadAsync(route); @@ -110,7 +116,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""id"": """ + meetings[0].StringId + @""", ""attributes"": { ""title"": """ + meetings[0].Title + @""", - ""startTime"": """ + meetings[0].StartTime.ToString("O") + @""", + ""startTime"": """ + meetings[0].StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", ""duration"": """ + meetings[0].Duration + @""", ""location"": { ""lat"": " + meetings[0].Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", @@ -191,7 +197,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""id"": """ + meetings[0].StringId + @""", ""attributes"": { ""title"": """ + meetings[0].Title + @""", - ""startTime"": """ + meetings[0].StartTime.ToString("O") + @""", + ""startTime"": """ + meetings[0].StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", ""duration"": """ + meetings[0].Duration + @""", ""location"": { ""lat"": " + meetings[0].Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", @@ -228,7 +234,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/meetings/" + meeting.StringId; + string route = $"/meetings/{meeting.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -245,7 +251,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""id"": """ + meeting.StringId + @""", ""attributes"": { ""title"": """ + meeting.Title + @""", - ""startTime"": """ + meeting.StartTime.ToString("O") + @""", + ""startTime"": """ + meeting.StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", ""duration"": """ + meeting.Duration + @""", ""location"": { ""lat"": " + meeting.Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", @@ -271,9 +277,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_get_unknown_primary_resource_by_ID() { // Arrange - var unknownId = Guid.NewGuid(); + string meetingId = Unknown.StringId.For(); - string route = "/meetings/" + unknownId; + string route = $"/meetings/{meetingId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -281,10 +287,7 @@ public async Task Cannot_get_unknown_primary_resource_by_ID() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); - var jObject = JsonConvert.DeserializeObject(responseDocument); - jObject.Should().NotBeNull(); - - string errorId = jObject!["errors"].Should().NotBeNull().And.Subject.Select(element => (string)element["id"]).Single(); + string errorId = JsonApiStringConverter.ExtractErrorId(responseDocument); responseDocument.Should().BeJson(@"{ ""errors"": [ @@ -292,7 +295,7 @@ public async Task Cannot_get_unknown_primary_resource_by_ID() ""id"": """ + errorId + @""", ""status"": ""404"", ""title"": ""The requested resource does not exist."", - ""detail"": ""Resource of type 'meetings' with ID '" + unknownId + @"' does not exist."" + ""detail"": ""Resource of type 'meetings' with ID '" + meetingId + @"' does not exist."" } ] }"); @@ -328,7 +331,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""id"": """ + attendee.Meeting.StringId + @""", ""attributes"": { ""title"": """ + attendee.Meeting.Title + @""", - ""startTime"": """ + attendee.Meeting.StartTime.ToString("O") + @""", + ""startTime"": """ + attendee.Meeting.StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", ""duration"": """ + attendee.Meeting.Duration + @""", ""location"": { ""lat"": " + attendee.Meeting.Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", @@ -575,7 +578,7 @@ public async Task Can_create_resource_with_side_effects() ""id"": """ + newMeeting.StringId + @""", ""attributes"": { ""title"": """ + newMeeting.Title + @""", - ""startTime"": """ + newMeeting.StartTime.ToString("O") + @""", + ""startTime"": """ + newMeeting.StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", ""duration"": """ + newMeeting.Duration + @""", ""location"": { ""lat"": " + newMeeting.Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", @@ -622,7 +625,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/meetingAttendees/" + existingAttendee.StringId; + string route = $"/meetingAttendees/{existingAttendee.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -655,6 +658,52 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }"); } + [Fact] + public async Task Can_update_resource_with_relationship_for_type_at_end() + { + // Arrange + MeetingAttendee existingAttendee = _fakers.MeetingAttendee.Generate(); + existingAttendee.Meeting = _fakers.Meeting.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Attendees.Add(existingAttendee); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + id = existingAttendee.StringId, + attributes = new + { + displayName = existingAttendee.DisplayName + }, + relationships = new + { + meeting = new + { + data = new + { + id = existingAttendee.Meeting.StringId, + type = "meetings" + } + } + }, + type = "meetingAttendees" + } + }; + + string route = $"/meetingAttendees/{existingAttendee.StringId}"; + + // Act + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + [Fact] public async Task Includes_version_on_resource_endpoint() { @@ -686,6 +735,40 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""self"": ""http://localhost/meetingAttendees/" + attendee.StringId + @"/meeting"" }, ""data"": null +}"); + } + + [Fact] + public async Task Includes_version_on_error_in_resource_endpoint() + { + // Arrange + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); + options.IncludeJsonApiVersion = true; + + string attendeeId = Unknown.StringId.For(); + + string route = $"/meetingAttendees/{attendeeId}"; + + // Act + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + + string errorId = JsonApiStringConverter.ExtractErrorId(responseDocument); + + responseDocument.Should().BeJson(@"{ + ""jsonapi"": { + ""version"": ""1.1"" + }, + ""errors"": [ + { + ""id"": """ + errorId + @""", + ""status"": ""404"", + ""title"": ""The requested resource does not exist."", + ""detail"": ""Resource of type 'meetingAttendees' with ID '" + attendeeId + @"' does not exist."" + } + ] }"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs index 18a6ac65cb..702aa7cee3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs @@ -65,8 +65,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(departments[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(departments[1].StringId); } [Fact] @@ -97,8 +97,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(departments[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(departments[0].StringId); } [Fact] @@ -128,9 +128,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Type.Should().Be("companies"); - responseDocument.ManyData[0].Id.Should().Be(companies[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Type.Should().Be("companies"); + responseDocument.Data.ManyValue[0].Id.Should().Be(companies[1].StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("departments"); @@ -150,17 +150,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/departments/" + department.StringId; + string route = $"/departments/{department.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'departments' with ID '{department.StringId}' does not exist."); @@ -183,14 +183,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{company.StringId}/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'companies' with ID '{company.StringId}' does not exist."); @@ -218,8 +218,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(company.Departments.ElementAt(1).StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(company.Departments.ElementAt(1).StringId); } [Fact] @@ -239,14 +239,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/departments/{department.StringId}/company"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'departments' with ID '{department.StringId}' does not exist."); @@ -274,7 +274,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.Should().BeNull(); + responseDocument.Data.Value.Should().BeNull(); } [Fact] @@ -294,14 +294,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{company.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'companies' with ID '{company.StringId}' does not exist."); @@ -329,8 +329,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(company.Departments.ElementAt(1).StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(company.Departments.ElementAt(1).StringId); } [Fact] @@ -350,14 +350,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/departments/{department.StringId}/relationships/company"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'departments' with ID '{department.StringId}' does not exist."); @@ -385,7 +385,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.Should().BeNull(); + responseDocument.Data.Value.Should().BeNull(); } [Fact] @@ -432,14 +432,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/companies"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); @@ -488,14 +488,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'companies' with ID '{existingCompany.StringId}' in relationship 'company' does not exist."); @@ -529,17 +529,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/companies/" + existingCompany.StringId; + string route = $"/companies/{existingCompany.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'companies' with ID '{existingCompany.StringId}' does not exist."); @@ -583,17 +583,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/companies/" + existingCompany.StringId; + string route = $"/companies/{existingCompany.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); @@ -636,17 +636,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/departments/" + existingDepartment.StringId; + string route = $"/departments/{existingDepartment.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'companies' with ID '{existingCompany.StringId}' in relationship 'company' does not exist."); @@ -674,14 +674,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{existingCompany.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'companies' with ID '{existingCompany.StringId}' does not exist."); @@ -717,14 +717,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{existingCompany.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); @@ -753,14 +753,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/departments/{existingDepartment.StringId}/relationships/company"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'departments' with ID '{existingDepartment.StringId}' does not exist."); @@ -793,14 +793,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/departments/{existingDepartment.StringId}/relationships/company"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'companies' with ID '{existingCompany.StringId}' in relationship 'company' does not exist."); @@ -836,14 +836,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{existingCompany.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'companies' with ID '{existingCompany.StringId}' does not exist."); @@ -879,14 +879,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{existingCompany.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); @@ -923,14 +923,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{existingCompany.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'companies' with ID '{existingCompany.StringId}' does not exist."); @@ -965,14 +965,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{existingCompany.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); @@ -992,7 +992,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/companies/" + existingCompany.StringId; + string route = $"/companies/{existingCompany.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -1024,17 +1024,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/departments/" + existingDepartment.StringId; + string route = $"/departments/{existingDepartment.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'departments' with ID '{existingDepartment.StringId}' does not exist."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs index 37f982762c..5fa4761150 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs @@ -54,9 +54,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be("00000000-0000-0000-0000-000000000000"); - responseDocument.ManyData[0].Links.Self.Should().Be("/maps/00000000-0000-0000-0000-000000000000"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be("00000000-0000-0000-0000-000000000000"); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be("/maps/00000000-0000-0000-0000-000000000000"); } [Fact] @@ -83,9 +83,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be("00000000-0000-0000-0000-000000000000"); - responseDocument.SingleData.Links.Self.Should().Be("/maps/00000000-0000-0000-0000-000000000000"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be("00000000-0000-0000-0000-000000000000"); + responseDocument.Data.SingleValue.Links.Self.Should().Be("/maps/00000000-0000-0000-0000-000000000000"); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be("0"); @@ -157,7 +157,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "maps", - id = Guid.Empty, + id = "00000000-0000-0000-0000-000000000000", attributes = new { name = newName @@ -244,7 +244,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "maps", - id = Guid.Empty + id = "00000000-0000-0000-0000-000000000000" } }; @@ -289,7 +289,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "maps", - id = Guid.Empty + id = "00000000-0000-0000-0000-000000000000" } }; diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs index a1e54cfd59..04133f348b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs @@ -54,9 +54,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be("0"); - responseDocument.ManyData[0].Links.Self.Should().Be("/games/0"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be("0"); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be("/games/0"); } [Fact] @@ -82,9 +82,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be("0"); - responseDocument.SingleData.Links.Self.Should().Be("/games/0"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be("0"); + responseDocument.Data.SingleValue.Links.Self.Should().Be("/games/0"); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(game.ActivePlayers.ElementAt(0).StringId); @@ -124,8 +124,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Headers.Location.Should().Be("/games/0"); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be("0"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be("0"); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -173,9 +173,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be("0"); - responseDocument.SingleData.Attributes["title"].Should().Be(newTitle); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be("0"); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(newTitle); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -246,7 +246,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "games", - id = 0 + id = "0" } }; @@ -376,7 +376,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "games", - id = 0 + id = "0" } } }; @@ -425,7 +425,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "games", - id = 0 + id = "0" } } }; @@ -474,7 +474,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "games", - id = 0 + id = "0" } } }; @@ -521,7 +521,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "games", - id = 0 + id = "0" } } }; diff --git a/test/JsonApiDotNetCoreTests/JsonApiDotNetCoreTests.csproj b/test/JsonApiDotNetCoreTests/JsonApiDotNetCoreTests.csproj index c313e73850..efb9e23173 100644 --- a/test/JsonApiDotNetCoreTests/JsonApiDotNetCoreTests.csproj +++ b/test/JsonApiDotNetCoreTests/JsonApiDotNetCoreTests.csproj @@ -15,6 +15,7 @@ + diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/DefaultsParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/DefaultsParseTests.cs deleted file mode 100644 index f775273cbb..0000000000 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/DefaultsParseTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Net; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers.Annotations; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.QueryStrings.Internal; -using Newtonsoft.Json; -using Xunit; - -namespace JsonApiDotNetCoreTests.UnitTests.QueryStringParameters -{ - public sealed class DefaultsParseTests - { - private readonly IDefaultsQueryStringParameterReader _reader; - - public DefaultsParseTests() - { - _reader = new DefaultsQueryStringParameterReader(new JsonApiOptions()); - } - - [Theory] - [InlineData("defaults", true)] - [InlineData("default", false)] - [InlineData("defaultsettings", false)] - public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanParse) - { - // Act - bool canParse = _reader.CanRead(parameterName); - - // Assert - canParse.Should().Be(expectCanParse); - } - - [Theory] - [InlineData(JsonApiQueryStringParameters.Defaults, false, false)] - [InlineData(JsonApiQueryStringParameters.Defaults, true, false)] - [InlineData(JsonApiQueryStringParameters.All, false, false)] - [InlineData(JsonApiQueryStringParameters.All, true, false)] - [InlineData(JsonApiQueryStringParameters.None, false, false)] - [InlineData(JsonApiQueryStringParameters.None, true, true)] - [InlineData(JsonApiQueryStringParameters.Filter, false, false)] - [InlineData(JsonApiQueryStringParameters.Filter, true, true)] - public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, bool allowOverride, bool expectIsEnabled) - { - // Arrange - var options = new JsonApiOptions - { - AllowQueryStringOverrideForSerializerDefaultValueHandling = allowOverride - }; - - var reader = new DefaultsQueryStringParameterReader(options); - - // Act - bool isEnabled = reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); - - // Assert - isEnabled.Should().Be(allowOverride && expectIsEnabled); - } - - [Theory] - [InlineData("defaults", "", "The value '' must be 'true' or 'false'.")] - [InlineData("defaults", " ", "The value ' ' must be 'true' or 'false'.")] - [InlineData("defaults", "null", "The value 'null' must be 'true' or 'false'.")] - [InlineData("defaults", "0", "The value '0' must be 'true' or 'false'.")] - [InlineData("defaults", "1", "The value '1' must be 'true' or 'false'.")] - [InlineData("defaults", "-1", "The value '-1' must be 'true' or 'false'.")] - public void Reader_Read_Fails(string parameterName, string parameterValue, string errorMessage) - { - // Act - Action action = () => _reader.Read(parameterName, parameterValue); - - // Assert - InvalidQueryStringParameterException exception = action.Should().ThrowExactly().And; - - exception.QueryParameterName.Should().Be(parameterName); - exception.Errors.Should().HaveCount(1); - exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - exception.Errors[0].Title.Should().Be("The specified defaults is invalid."); - exception.Errors[0].Detail.Should().Be(errorMessage); - exception.Errors[0].Source.Parameter.Should().Be(parameterName); - } - - [Theory] - [InlineData("defaults", "true", DefaultValueHandling.Include)] - [InlineData("defaults", "True", DefaultValueHandling.Include)] - [InlineData("defaults", "false", DefaultValueHandling.Ignore)] - [InlineData("defaults", "False", DefaultValueHandling.Ignore)] - public void Reader_Read_Succeeds(string parameterName, string parameterValue, DefaultValueHandling expectedValue) - { - // Act - _reader.Read(parameterName, parameterValue); - - DefaultValueHandling handling = _reader.SerializerDefaultValueHandling; - - // Assert - handling.Should().Be(expectedValue); - } - - [Theory] - [InlineData("false", DefaultValueHandling.Ignore, false, DefaultValueHandling.Ignore)] - [InlineData("true", DefaultValueHandling.Ignore, false, DefaultValueHandling.Ignore)] - [InlineData("false", DefaultValueHandling.Include, false, DefaultValueHandling.Include)] - [InlineData("true", DefaultValueHandling.Include, false, DefaultValueHandling.Include)] - [InlineData("false", DefaultValueHandling.Ignore, true, DefaultValueHandling.Ignore)] - [InlineData("true", DefaultValueHandling.Ignore, true, DefaultValueHandling.Include)] - [InlineData("false", DefaultValueHandling.Include, true, DefaultValueHandling.Ignore)] - [InlineData("true", DefaultValueHandling.Include, true, DefaultValueHandling.Include)] - public void Reader_Outcome(string queryStringParameterValue, DefaultValueHandling optionsDefaultValue, bool optionsAllowOverride, - DefaultValueHandling expected) - { - // Arrange - var options = new JsonApiOptions - { - SerializerSettings = - { - DefaultValueHandling = optionsDefaultValue - }, - AllowQueryStringOverrideForSerializerDefaultValueHandling = optionsAllowOverride - }; - - var reader = new DefaultsQueryStringParameterReader(options); - - // Act - if (reader.IsEnabled(DisableQueryStringAttribute.Empty)) - { - reader.Read("defaults", queryStringParameterValue); - } - - // Assert - reader.SerializerDefaultValueHandling.Should().Be(expected); - } - } -} diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/NullsParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/NullsParseTests.cs deleted file mode 100644 index c5e83833b9..0000000000 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/NullsParseTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Net; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers.Annotations; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.QueryStrings.Internal; -using Newtonsoft.Json; -using Xunit; - -namespace JsonApiDotNetCoreTests.UnitTests.QueryStringParameters -{ - public sealed class NullsParseTests - { - private readonly INullsQueryStringParameterReader _reader; - - public NullsParseTests() - { - _reader = new NullsQueryStringParameterReader(new JsonApiOptions()); - } - - [Theory] - [InlineData("nulls", true)] - [InlineData("null", false)] - [InlineData("nullsettings", false)] - public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanParse) - { - // Act - bool canParse = _reader.CanRead(parameterName); - - // Assert - canParse.Should().Be(expectCanParse); - } - - [Theory] - [InlineData(JsonApiQueryStringParameters.Nulls, false, false)] - [InlineData(JsonApiQueryStringParameters.Nulls, true, false)] - [InlineData(JsonApiQueryStringParameters.All, false, false)] - [InlineData(JsonApiQueryStringParameters.All, true, false)] - [InlineData(JsonApiQueryStringParameters.None, false, false)] - [InlineData(JsonApiQueryStringParameters.None, true, true)] - [InlineData(JsonApiQueryStringParameters.Filter, false, false)] - [InlineData(JsonApiQueryStringParameters.Filter, true, true)] - public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, bool allowOverride, bool expectIsEnabled) - { - // Arrange - var options = new JsonApiOptions - { - AllowQueryStringOverrideForSerializerNullValueHandling = allowOverride - }; - - var reader = new NullsQueryStringParameterReader(options); - - // Act - bool isEnabled = reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); - - // Assert - isEnabled.Should().Be(allowOverride && expectIsEnabled); - } - - [Theory] - [InlineData("nulls", "", "The value '' must be 'true' or 'false'.")] - [InlineData("nulls", " ", "The value ' ' must be 'true' or 'false'.")] - [InlineData("nulls", "null", "The value 'null' must be 'true' or 'false'.")] - [InlineData("nulls", "0", "The value '0' must be 'true' or 'false'.")] - [InlineData("nulls", "1", "The value '1' must be 'true' or 'false'.")] - [InlineData("nulls", "-1", "The value '-1' must be 'true' or 'false'.")] - public void Reader_Read_Fails(string parameterName, string parameterValue, string errorMessage) - { - // Act - Action action = () => _reader.Read(parameterName, parameterValue); - - // Assert - InvalidQueryStringParameterException exception = action.Should().ThrowExactly().And; - - exception.QueryParameterName.Should().Be(parameterName); - exception.Errors.Should().HaveCount(1); - exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - exception.Errors[0].Title.Should().Be("The specified nulls is invalid."); - exception.Errors[0].Detail.Should().Be(errorMessage); - exception.Errors[0].Source.Parameter.Should().Be(parameterName); - } - - [Theory] - [InlineData("nulls", "true", NullValueHandling.Include)] - [InlineData("nulls", "True", NullValueHandling.Include)] - [InlineData("nulls", "false", NullValueHandling.Ignore)] - [InlineData("nulls", "False", NullValueHandling.Ignore)] - public void Reader_Read_Succeeds(string parameterName, string parameterValue, NullValueHandling expectedValue) - { - // Act - _reader.Read(parameterName, parameterValue); - - NullValueHandling handling = _reader.SerializerNullValueHandling; - - // Assert - handling.Should().Be(expectedValue); - } - - [Theory] - [InlineData("false", NullValueHandling.Ignore, false, NullValueHandling.Ignore)] - [InlineData("true", NullValueHandling.Ignore, false, NullValueHandling.Ignore)] - [InlineData("false", NullValueHandling.Include, false, NullValueHandling.Include)] - [InlineData("true", NullValueHandling.Include, false, NullValueHandling.Include)] - [InlineData("false", NullValueHandling.Ignore, true, NullValueHandling.Ignore)] - [InlineData("true", NullValueHandling.Ignore, true, NullValueHandling.Include)] - [InlineData("false", NullValueHandling.Include, true, NullValueHandling.Ignore)] - [InlineData("true", NullValueHandling.Include, true, NullValueHandling.Include)] - public void Reader_Outcome(string queryStringParameterValue, NullValueHandling optionsNullValue, bool optionsAllowOverride, NullValueHandling expected) - { - // Arrange - var options = new JsonApiOptions - { - SerializerSettings = - { - NullValueHandling = optionsNullValue - }, - AllowQueryStringOverrideForSerializerNullValueHandling = optionsAllowOverride - }; - - var reader = new NullsQueryStringParameterReader(options); - - // Act - if (reader.IsEnabled(DisableQueryStringAttribute.Empty)) - { - reader.Read("nulls", queryStringParameterValue); - } - - // Assert - reader.SerializerNullValueHandling.Should().Be(expected); - } - } -} diff --git a/test/MultiDbContextTests/ResourceTests.cs b/test/MultiDbContextTests/ResourceTests.cs index ece21a33a4..27b2049e07 100644 --- a/test/MultiDbContextTests/ResourceTests.cs +++ b/test/MultiDbContextTests/ResourceTests.cs @@ -1,9 +1,12 @@ using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; using MultiDbContextExample; using TestBuildingBlocks; using Xunit; @@ -14,6 +17,15 @@ public sealed class ResourceTests : IntegrationTest, IClassFixture _factory; + protected override JsonSerializerOptions SerializerOptions + { + get + { + var options = _factory.Services.GetRequiredService(); + return options.SerializerOptions; + } + } + public ResourceTests(WebApplicationFactory factory) { _factory = factory; @@ -31,8 +43,8 @@ public async Task Can_get_ResourceAs() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["nameA"].Should().Be("SampleA"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["nameA"].Should().Be("SampleA"); } [Fact] @@ -47,8 +59,8 @@ public async Task Can_get_ResourceBs() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["nameB"].Should().Be("SampleB"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["nameB"].Should().Be("SampleB"); } protected override HttpClient CreateClient() diff --git a/test/NoEntityFrameworkTests/WorkItemTests.cs b/test/NoEntityFrameworkTests/WorkItemTests.cs index 2bf2f64c2a..e1b6dbafe6 100644 --- a/test/NoEntityFrameworkTests/WorkItemTests.cs +++ b/test/NoEntityFrameworkTests/WorkItemTests.cs @@ -1,8 +1,10 @@ using System; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; @@ -18,6 +20,15 @@ public sealed class WorkItemTests : IntegrationTest, IClassFixture _factory; + protected override JsonSerializerOptions SerializerOptions + { + get + { + var options = _factory.Services.GetRequiredService(); + return options.SerializerOptions; + } + } + public WorkItemTests(WebApplicationFactory factory) { _factory = factory; @@ -41,7 +52,7 @@ await RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().NotBeEmpty(); + responseDocument.Data.ManyValue.Should().NotBeEmpty(); } [Fact] @@ -56,7 +67,7 @@ await RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/api/v1/workItems/" + workItem.StringId; + string route = $"/api/v1/workItems/{workItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); @@ -64,8 +75,8 @@ await RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(workItem.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(workItem.StringId); } [Fact] @@ -103,11 +114,11 @@ public async Task Can_create_WorkItem() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["isBlocked"].Should().Be(newWorkItem.IsBlocked); - responseDocument.SingleData.Attributes["title"].Should().Be(newWorkItem.Title); - responseDocument.SingleData.Attributes["durationInHours"].Should().Be(newWorkItem.DurationInHours); - responseDocument.SingleData.Attributes["projectId"].Should().Be(newWorkItem.ProjectId.ToString()); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["isBlocked"].Should().Be(newWorkItem.IsBlocked); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(newWorkItem.Title); + responseDocument.Data.SingleValue.Attributes["durationInHours"].Should().Be(newWorkItem.DurationInHours); + responseDocument.Data.SingleValue.Attributes["projectId"].Should().Be(newWorkItem.ProjectId); } [Fact] @@ -122,7 +133,7 @@ await RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/api/v1/workItems/" + workItem.StringId; + string route = $"/api/v1/workItems/{workItem.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await ExecuteDeleteAsync(route); diff --git a/test/TestBuildingBlocks/DbContextExtensions.cs b/test/TestBuildingBlocks/DbContextExtensions.cs index 329d6f1633..389038f7d1 100644 --- a/test/TestBuildingBlocks/DbContextExtensions.cs +++ b/test/TestBuildingBlocks/DbContextExtensions.cs @@ -51,11 +51,11 @@ private static async Task ClearTablesAsync(this DbContext dbContext, params Type // In that case, we recursively delete all related data, which is slow. try { - await dbContext.Database.ExecuteSqlRawAsync("delete from \"" + tableName + "\""); + await dbContext.Database.ExecuteSqlRawAsync($"delete from \"{tableName}\""); } catch (PostgresException) { - await dbContext.Database.ExecuteSqlRawAsync("truncate table \"" + tableName + "\" cascade"); + await dbContext.Database.ExecuteSqlRawAsync($"truncate table \"{tableName}\" cascade"); } } } diff --git a/test/TestBuildingBlocks/FakerContainer.cs b/test/TestBuildingBlocks/FakerContainer.cs index a324c9dacb..2dbfb441b8 100644 --- a/test/TestBuildingBlocks/FakerContainer.cs +++ b/test/TestBuildingBlocks/FakerContainer.cs @@ -13,7 +13,7 @@ protected static int GetFakerSeed() // The goal here is to have stable data over multiple test runs, but at the same time different data per test case. MethodBase testMethod = GetTestMethod(); - string testName = testMethod.DeclaringType?.FullName + "." + testMethod.Name; + string testName = $"{testMethod.DeclaringType?.FullName}.{testMethod.Name}"; return GetDeterministicHashCode(testName); } diff --git a/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs b/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs index 681166358a..873cec6d3f 100644 --- a/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs +++ b/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs @@ -1,11 +1,8 @@ using System.Net; using System.Net.Http; -using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Primitives; using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace TestBuildingBlocks { @@ -22,8 +19,8 @@ public sealed class HttpResponseMessageAssertions : ReferenceTypeAssertions "response"; public HttpResponseMessageAssertions(HttpResponseMessage instance) + : base(instance) { - Subject = instance; } // ReSharper disable once UnusedMethodReturnValue.Global @@ -32,33 +29,12 @@ public AndConstraint HaveStatusCode(HttpStatusCod { if (Subject.StatusCode != statusCode) { - string responseText = GetFormattedContentAsync(Subject).Result; - Subject.StatusCode.Should().Be(statusCode, "response body returned was:\n" + responseText); + string responseText = Subject.Content.ReadAsStringAsync().Result; + Subject.StatusCode.Should().Be(statusCode, $"response body returned was:\n{responseText}"); } return new AndConstraint(this); } - - private static async Task GetFormattedContentAsync(HttpResponseMessage responseMessage) - { - string text = await responseMessage.Content.ReadAsStringAsync(); - - try - { - if (text.Length > 0) - { - return JsonConvert.DeserializeObject(text)?.ToString(); - } - } -#pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException - catch -#pragma warning restore AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException - { - // ignored - } - - return text; - } } } } diff --git a/test/TestBuildingBlocks/IntegrationTest.cs b/test/TestBuildingBlocks/IntegrationTest.cs index 6de6ca28a5..59e45b0b72 100644 --- a/test/TestBuildingBlocks/IntegrationTest.cs +++ b/test/TestBuildingBlocks/IntegrationTest.cs @@ -2,9 +2,9 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using JsonApiDotNetCore.Middleware; -using Newtonsoft.Json; namespace TestBuildingBlocks { @@ -13,7 +13,7 @@ namespace TestBuildingBlocks /// public abstract class IntegrationTest { - private static readonly IntegrationTestConfiguration IntegrationTestConfiguration = new(); + protected abstract JsonSerializerOptions SerializerOptions { get; } public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteHeadAsync(string requestUrl, Action setRequestHeaders = null) @@ -82,7 +82,8 @@ public abstract class IntegrationTest private string SerializeRequest(object requestBody) { - return requestBody == null ? null : requestBody is string stringRequestBody ? stringRequestBody : JsonConvert.SerializeObject(requestBody); + return requestBody == null ? null : + requestBody is string stringRequestBody ? stringRequestBody : JsonSerializer.Serialize(requestBody, SerializerOptions); } protected abstract HttpClient CreateClient(); @@ -96,7 +97,7 @@ private TResponseDocument DeserializeResponse(string response try { - return JsonConvert.DeserializeObject(responseText, IntegrationTestConfiguration.DeserializationSettings); + return JsonSerializer.Deserialize(responseText, SerializerOptions); } catch (JsonException exception) { diff --git a/test/TestBuildingBlocks/IntegrationTestConfiguration.cs b/test/TestBuildingBlocks/IntegrationTestConfiguration.cs deleted file mode 100644 index 30c1c0328e..0000000000 --- a/test/TestBuildingBlocks/IntegrationTestConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace TestBuildingBlocks -{ - internal sealed class IntegrationTestConfiguration - { - // Because our tests often deserialize incoming responses into weakly-typed string-to-object dictionaries (as part of ResourceObject), - // Newtonsoft.JSON is unable to infer the target type in such cases. So we steer a bit using explicit configuration. - public readonly JsonSerializerSettings DeserializationSettings = new() - { - // Choosing between DateTime and DateTimeOffset is impossible: it depends on how the resource properties are declared. - // So instead we leave them as strings and let the test itself deal with the conversion. - DateParseHandling = DateParseHandling.None, - - // Here we must choose between double (default) and decimal. Favored decimal because it has higher precision (but lower range). - FloatParseHandling = FloatParseHandling.Decimal - }; - } -} diff --git a/test/TestBuildingBlocks/IntegrationTestContext.cs b/test/TestBuildingBlocks/IntegrationTestContext.cs index b690374c93..68550f4c26 100644 --- a/test/TestBuildingBlocks/IntegrationTestContext.cs +++ b/test/TestBuildingBlocks/IntegrationTestContext.cs @@ -1,7 +1,9 @@ using System; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Testing; @@ -35,6 +37,15 @@ public class IntegrationTestContext : IntegrationTest, IDi private Action _beforeServicesConfiguration; private Action _afterServicesConfiguration; + protected override JsonSerializerOptions SerializerOptions + { + get + { + var options = Factory.Services.GetRequiredService(); + return options.SerializerOptions; + } + } + public WebApplicationFactory Factory => _lazyFactory.Value; public IntegrationTestContext() diff --git a/test/TestBuildingBlocks/JsonApiStringConverter.cs b/test/TestBuildingBlocks/JsonApiStringConverter.cs new file mode 100644 index 0000000000..40b334cac1 --- /dev/null +++ b/test/TestBuildingBlocks/JsonApiStringConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; +using System.Text.Json; + +#pragma warning disable AV1008 // Class should not be static +#pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException + +namespace TestBuildingBlocks +{ + public static class JsonApiStringConverter + { + public static string ExtractErrorId(string responseBody) + { + try + { + using JsonDocument document = JsonDocument.Parse(responseBody); + return document.RootElement.GetProperty("errors").EnumerateArray().Single().GetProperty("id").GetString(); + } + catch (Exception exception) + { + throw new Exception($"Failed to extract Error ID from response body '{responseBody}'.", exception); + } + } + } +} diff --git a/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs b/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs index 3efdd034a3..90edc1a544 100644 --- a/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs +++ b/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs @@ -1,10 +1,12 @@ using System; +using System.IO; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; using FluentAssertions; using FluentAssertions.Numeric; using FluentAssertions.Primitives; using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace TestBuildingBlocks { @@ -12,33 +14,34 @@ namespace TestBuildingBlocks public static class ObjectAssertionsExtensions { private const decimal NumericPrecision = 0.00000000001M; + private static readonly TimeSpan TimePrecision = TimeSpan.FromMilliseconds(20); - private static readonly JsonSerializerSettings DeserializationSettings = new() + private static readonly JsonWriterOptions JsonWriterOptions = new() { - Formatting = Formatting.Indented + Indented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; /// - /// Used to assert on a (nullable) or property, whose value is returned as in - /// JSON:API response body because of . + /// Same as , but with default precision. /// [CustomAssertion] - public static void BeCloseTo(this ObjectAssertions source, DateTimeOffset? expected, string because = "", params object[] becauseArgs) + public static AndConstraint BeCloseTo(this DateTimeAssertions parent, DateTime nearbyTime, string because = "", + params object[] becauseArgs) + where TAssertions : DateTimeAssertions { - if (expected == null) - { - source.Subject.Should().BeNull(because, becauseArgs); - } - else - { - if (!DateTimeOffset.TryParse((string)source.Subject, out DateTimeOffset value)) - { - source.Subject.Should().Be(expected, because, becauseArgs); - } + return parent.BeCloseTo(nearbyTime, TimePrecision, because, becauseArgs); + } - // We lose a little bit of precision (milliseconds) on roundtrip through PostgreSQL database. - value.Should().BeCloseTo(expected.Value, because: because, becauseArgs: becauseArgs); - } + /// + /// Same as , but with default precision. + /// + [CustomAssertion] + public static AndConstraint BeCloseTo(this DateTimeOffsetAssertions parent, DateTimeOffset nearbyTime, + string because = "", params object[] becauseArgs) + where TAssertions : DateTimeOffsetAssertions + { + return parent.BeCloseTo(nearbyTime, TimePrecision, because, becauseArgs); } /// @@ -69,13 +72,23 @@ public static AndConstraint> BeApproximately( [CustomAssertion] public static void BeJson(this StringAssertions source, string expected, string because = "", params object[] becauseArgs) { - var sourceToken = JsonConvert.DeserializeObject(source.Subject, DeserializationSettings); - var expectedToken = JsonConvert.DeserializeObject(expected, DeserializationSettings); + using JsonDocument sourceJson = JsonDocument.Parse(source.Subject); + using JsonDocument expectedJson = JsonDocument.Parse(expected); - string sourceText = sourceToken?.ToString(); - string expectedText = expectedToken?.ToString(); + string sourceText = ToJsonString(sourceJson); + string expectedText = ToJsonString(expectedJson); sourceText.Should().Be(expectedText, because, becauseArgs); } + + private static string ToJsonString(JsonDocument document) + { + using var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream, JsonWriterOptions); + + document.WriteTo(writer); + writer.Flush(); + return Encoding.UTF8.GetString(stream.ToArray()); + } } } diff --git a/test/TestBuildingBlocks/TestableStartup.cs b/test/TestBuildingBlocks/TestableStartup.cs index 93705dfb2d..3f029e4eb6 100644 --- a/test/TestBuildingBlocks/TestableStartup.cs +++ b/test/TestBuildingBlocks/TestableStartup.cs @@ -4,8 +4,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace TestBuildingBlocks { @@ -20,8 +18,7 @@ public virtual void ConfigureServices(IServiceCollection services) protected virtual void SetJsonApiOptions(JsonApiOptions options) { options.IncludeExceptionStackTraceInErrors = true; - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.Converters.Add(new StringEnumConverter()); + options.SerializerOptions.WriteIndented = true; } public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory) diff --git a/test/TestBuildingBlocks/Unknown.cs b/test/TestBuildingBlocks/Unknown.cs new file mode 100644 index 0000000000..294490d96e --- /dev/null +++ b/test/TestBuildingBlocks/Unknown.cs @@ -0,0 +1,87 @@ +using System; +using JsonApiDotNetCore.Resources; + +// ReSharper disable MemberCanBeInternal +// ReSharper disable MemberCanBePrivate.Global +#pragma warning disable AV1008 // Class should not be static + +namespace TestBuildingBlocks +{ + public static class Unknown + { + public const string ResourceType = "doesNotExist1"; + public const string Relationship = "doesNotExist2"; + public const string LocalId = "doesNotExist3"; + + public static class TypedId + { + public const short Int16 = short.MaxValue; + public const short AltInt16 = Int16 - 1; + + public const int Int32 = int.MaxValue; + public const int AltInt32 = Int32 - 1; + + public const long Int64 = long.MaxValue; + public const long AltInt64 = Int64 - 1; + + public static readonly Guid Guid = Guid.Parse("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"); + public static readonly Guid AltGuid = Guid.Parse("EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE"); + } + + public static class StringId + { + public static readonly string Int16 = TypedId.Int16.ToString(); + public static readonly string AltInt16 = TypedId.AltInt16.ToString(); + + public static readonly string Int32 = TypedId.Int32.ToString(); + public static readonly string AltInt32 = TypedId.AltInt32.ToString(); + + public static readonly string Int64 = TypedId.Int64.ToString(); + public static readonly string AltInt64 = TypedId.AltInt64.ToString(); + + public static readonly string Guid = TypedId.Guid.ToString(); + public static readonly string AltGuid = TypedId.AltGuid.ToString(); + + public static string For() + where TResource : IIdentifiable + { + return InnerFor(false); + } + + public static string AltFor() + where TResource : IIdentifiable + { + return InnerFor(true); + } + + private static string InnerFor(bool isAlt) + where TResource : IIdentifiable + { + Type type = typeof(TId); + + if (type == typeof(short)) + { + return isAlt ? AltInt16 : Int16; + } + + if (type == typeof(int)) + { + return isAlt ? AltInt32 : Int32; + } + + if (type == typeof(long)) + { + return isAlt ? AltInt64 : Int64; + } + + if (type == typeof(Guid)) + { + return isAlt ? AltGuid : Guid; + } + + throw new NotSupportedException( + $"Unsupported '{nameof(Identifiable.Id)}' property of type '{type}' on resource type '{typeof(TResource).Name}'."); + } + } + } +} diff --git a/test/UnitTests/Builders/ResourceGraphBuilderTests.cs b/test/UnitTests/Builders/ResourceGraphBuilderTests.cs index 69e5efe56f..b6cd276c99 100644 --- a/test/UnitTests/Builders/ResourceGraphBuilderTests.cs +++ b/test/UnitTests/Builders/ResourceGraphBuilderTests.cs @@ -29,10 +29,10 @@ public void Can_Build_ResourceGraph_Using_Builder() // Assert var resourceGraph = container.GetRequiredService(); - ResourceContext dbResource = resourceGraph.GetResourceContext("dbResources"); - ResourceContext nonDbResource = resourceGraph.GetResourceContext("nonDbResources"); - Assert.Equal(typeof(DbResource), dbResource.ResourceType); - Assert.Equal(typeof(NonDbResource), nonDbResource.ResourceType); + ResourceContext dbResourceContext = resourceGraph.GetResourceContext("dbResources"); + ResourceContext nonDbResourceContext = resourceGraph.GetResourceContext("nonDbResources"); + Assert.Equal(typeof(DbResource), dbResourceContext.ResourceType); + Assert.Equal(typeof(NonDbResource), nonDbResourceContext.ResourceType); } [Fact] @@ -46,8 +46,8 @@ public void Resources_Without_Names_Specified_Will_Use_Configured_Formatter() IResourceGraph resourceGraph = builder.Build(); // Assert - ResourceContext resource = resourceGraph.GetResourceContext(typeof(TestResource)); - Assert.Equal("testResources", resource.PublicName); + ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(TestResource)); + Assert.Equal("testResources", resourceContext.PublicName); } [Fact] @@ -61,8 +61,8 @@ public void Attrs_Without_Names_Specified_Will_Use_Configured_Formatter() IResourceGraph resourceGraph = builder.Build(); // Assert - ResourceContext resource = resourceGraph.GetResourceContext(typeof(TestResource)); - Assert.Contains(resource.Attributes, attribute => attribute.PublicName == "compoundAttribute"); + ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(TestResource)); + Assert.Contains(resourceContext.Attributes, attribute => attribute.PublicName == "compoundAttribute"); } [Fact] @@ -76,9 +76,9 @@ public void Relationships_Without_Names_Specified_Will_Use_Configured_Formatter( IResourceGraph resourceGraph = builder.Build(); // Assert - ResourceContext resource = resourceGraph.GetResourceContext(typeof(TestResource)); - Assert.Equal("relatedResource", resource.Relationships.Single(relationship => relationship is HasOneAttribute).PublicName); - Assert.Equal("relatedResources", resource.Relationships.Single(relationship => relationship is not HasOneAttribute).PublicName); + ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(TestResource)); + Assert.Equal("relatedResource", resourceContext.Relationships.Single(relationship => relationship is HasOneAttribute).PublicName); + Assert.Equal("relatedResources", resourceContext.Relationships.Single(relationship => relationship is not HasOneAttribute).PublicName); } private sealed class NonDbResource : Identifiable diff --git a/test/UnitTests/Controllers/BaseJsonApiControllerTests.cs b/test/UnitTests/Controllers/BaseJsonApiControllerTests.cs index 4b5ef50e47..90c0426d66 100644 --- a/test/UnitTests/Controllers/BaseJsonApiControllerTests.cs +++ b/test/UnitTests/Controllers/BaseJsonApiControllerTests.cs @@ -26,7 +26,7 @@ public async Task GetAsync_Calls_Service() { // Arrange var serviceMock = new Mock>(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, serviceMock.Object); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, serviceMock.Object); // Act await controller.GetAsync(CancellationToken.None); @@ -39,7 +39,7 @@ public async Task GetAsync_Calls_Service() public async Task GetAsync_Throws_405_If_No_Service() { // Arrange - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, null); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, null); // Act Func asyncAction = () => controller.GetAsync(CancellationToken.None); @@ -56,7 +56,7 @@ public async Task GetAsyncById_Calls_Service() // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, getById: serviceMock.Object); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, getById: serviceMock.Object); // Act await controller.GetAsync(id, CancellationToken.None); @@ -70,7 +70,7 @@ public async Task GetAsyncById_Throws_405_If_No_Service() { // Arrange const int id = 0; - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance); // Act Func asyncAction = () => controller.GetAsync(id, CancellationToken.None); @@ -88,7 +88,7 @@ public async Task GetRelationshipsAsync_Calls_Service() const int id = 0; const string relationshipName = "articles"; var serviceMock = new Mock>(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, getRelationship: serviceMock.Object); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, getRelationship: serviceMock.Object); // Act await controller.GetRelationshipAsync(id, relationshipName, CancellationToken.None); @@ -102,7 +102,7 @@ public async Task GetRelationshipsAsync_Throws_405_If_No_Service() { // Arrange const int id = 0; - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance); // Act Func asyncAction = () => controller.GetRelationshipAsync(id, "articles", CancellationToken.None); @@ -120,7 +120,7 @@ public async Task GetRelationshipAsync_Calls_Service() const int id = 0; const string relationshipName = "articles"; var serviceMock = new Mock>(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, getSecondary: serviceMock.Object); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, getSecondary: serviceMock.Object); // Act await controller.GetSecondaryAsync(id, relationshipName, CancellationToken.None); @@ -134,7 +134,7 @@ public async Task GetRelationshipAsync_Throws_405_If_No_Service() { // Arrange const int id = 0; - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance); // Act Func asyncAction = () => controller.GetSecondaryAsync(id, "articles", CancellationToken.None); @@ -168,7 +168,7 @@ public async Task PatchAsync_Throws_405_If_No_Service() // Arrange const int id = 0; var resource = new Resource(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance); // Act Func asyncAction = () => controller.PatchAsync(id, resource, CancellationToken.None); @@ -208,7 +208,7 @@ public async Task PatchRelationshipsAsync_Calls_Service() const int id = 0; const string relationshipName = "articles"; var serviceMock = new Mock>(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, setRelationship: serviceMock.Object); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, setRelationship: serviceMock.Object); // Act await controller.PatchRelationshipAsync(id, relationshipName, null, CancellationToken.None); @@ -222,7 +222,7 @@ public async Task PatchRelationshipsAsync_Throws_405_If_No_Service() { // Arrange const int id = 0; - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance); // Act Func asyncAction = () => controller.PatchRelationshipAsync(id, "articles", null, CancellationToken.None); @@ -239,7 +239,7 @@ public async Task DeleteAsync_Calls_Service() // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, delete: serviceMock.Object); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, delete: serviceMock.Object); // Act await controller.DeleteAsync(id, CancellationToken.None); @@ -253,7 +253,7 @@ public async Task DeleteAsync_Throws_405_If_No_Service() { // Arrange const int id = 0; - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance); // Act Func asyncAction = () => controller.DeleteAsync(id, CancellationToken.None); diff --git a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index 2d24270655..542abd496a 100644 --- a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -28,15 +28,17 @@ public void RegisterResource_DeviatingDbContextPropertyName_RegistersCorrectly() var services = new ServiceCollection(); services.AddLogging(); services.AddDbContext(options => options.UseInMemoryDatabase("UnitTestDb")); - services.AddJsonApi(); - // Act // this is required because the DbContextResolver requires access to the current HttpContext // to get the request scoped DbContext instance services.AddScoped(); + + // Act + services.AddJsonApi(); + ServiceProvider provider = services.BuildServiceProvider(); - var graph = provider.GetRequiredService(); - ResourceContext resourceContext = graph.GetResourceContext(); + var resourceGraph = provider.GetRequiredService(); + ResourceContext resourceContext = resourceGraph.GetResourceContext(); // Assert Assert.Equal("people", resourceContext.PublicName); @@ -177,8 +179,9 @@ public void AddJsonApi_With_Context_Uses_Resource_Type_Name_If_NoOtherSpecified( // Assert ServiceProvider provider = services.BuildServiceProvider(); var resourceGraph = provider.GetRequiredService(); - ResourceContext resource = resourceGraph.GetResourceContext(typeof(IntResource)); - Assert.Equal("intResources", resource.PublicName); + + ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(IntResource)); + Assert.Equal("intResources", resourceContext.PublicName); } private sealed class IntResource : Identifiable diff --git a/test/UnitTests/Internal/ErrorDocumentTests.cs b/test/UnitTests/Internal/ErrorDocumentTests.cs index 490ebcf91f..e2426659b2 100644 --- a/test/UnitTests/Internal/ErrorDocumentTests.cs +++ b/test/UnitTests/Internal/ErrorDocumentTests.cs @@ -18,7 +18,10 @@ public sealed class ErrorDocumentTests public void ErrorDocument_GetErrorStatusCode_IsCorrect(HttpStatusCode[] errorCodes, HttpStatusCode expected) { // Arrange - var document = new ErrorDocument(errorCodes.Select(code => new Error(code))); + var document = new Document + { + Errors = errorCodes.Select(code => new ErrorObject(code)).ToList() + }; // Act HttpStatusCode status = document.GetErrorStatusCode(); diff --git a/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs b/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs index 4da96708b0..49313b4364 100644 --- a/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs +++ b/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs @@ -24,7 +24,7 @@ public void When_http_context_is_unavailable_it_must_fail() // Assert var exception = Assert.Throws(action); - Assert.StartsWith("Cannot resolve scoped service " + $"'{serviceType.FullName}' outside the context of an HTTP request.", exception.Message); + Assert.StartsWith($"Cannot resolve scoped service '{serviceType.FullName}' outside the context of an HTTP request.", exception.Message); } [UsedImplicitly(ImplicitUseTargetFlags.Itself)] diff --git a/test/UnitTests/Internal/ResourceGraphBuilderTests.cs b/test/UnitTests/Internal/ResourceGraphBuilderTests.cs index 2c2b391a8b..d1df3947d8 100644 --- a/test/UnitTests/Internal/ResourceGraphBuilderTests.cs +++ b/test/UnitTests/Internal/ResourceGraphBuilderTests.cs @@ -57,10 +57,10 @@ public void GetResourceContext_Yields_Right_Type_For_LazyLoadingProxy() // Act var proxy = proxyGenerator.CreateClassProxy(); - ResourceContext result = resourceGraph.GetResourceContext(proxy.GetType()); + ResourceContext resourceContext = resourceGraph.GetResourceContext(proxy.GetType()); // Assert - Assert.Equal(typeof(Bar), result.ResourceType); + Assert.Equal(typeof(Bar), resourceContext.ResourceType); } [Fact] @@ -72,10 +72,10 @@ public void GetResourceContext_Yields_Right_Type_For_Identifiable() var resourceGraph = (ResourceGraph)resourceGraphBuilder.Build(); // Act - ResourceContext result = resourceGraph.GetResourceContext(typeof(Bar)); + ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(Bar)); // Assert - Assert.Equal(typeof(Bar), result.ResourceType); + Assert.Equal(typeof(Bar), resourceContext.ResourceType); } [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] diff --git a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs index 22c4f92f57..93122f9a99 100644 --- a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs +++ b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs @@ -79,7 +79,7 @@ private Task RunMiddlewareTask(InvokeConfiguration holder) { IControllerResourceMapping controllerResourceMapping = holder.ControllerResourceMapping.Object; HttpContext context = holder.HttpContext; - IJsonApiOptions options = holder.Options.Object; + IJsonApiOptions options = holder.Options; JsonApiRequest request = holder.Request; IResourceGraph resourceGraph = holder.ResourceGraph.Object; return holder.MiddleWare.InvokeAsync(context, controllerResourceMapping, options, request, resourceGraph, NullLogger.Instance); @@ -101,7 +101,7 @@ private InvokeConfiguration GetConfiguration(string path, string resourceName = var mockMapping = new Mock(); mockMapping.Setup(mapping => mapping.GetResourceTypeForController(It.IsAny())).Returns(typeof(string)); - Mock mockOptions = CreateMockOptions(forcedNamespace); + IJsonApiOptions options = CreateOptions(forcedNamespace); Mock mockGraph = CreateMockResourceGraph(resourceName, relType != null); var request = new JsonApiRequest(); @@ -119,18 +119,21 @@ private InvokeConfiguration GetConfiguration(string path, string resourceName = { MiddleWare = middleware, ControllerResourceMapping = mockMapping, - Options = mockOptions, + Options = options, Request = request, HttpContext = context, ResourceGraph = mockGraph }; } - private static Mock CreateMockOptions(string forcedNamespace) + private static IJsonApiOptions CreateOptions(string forcedNamespace) { - var mockOptions = new Mock(); - mockOptions.Setup(options => options.Namespace).Returns(forcedNamespace); - return mockOptions; + var options = new JsonApiOptions + { + Namespace = forcedNamespace + }; + + return options; } private static DefaultHttpContext CreateHttpContext(string path, bool isRelationship = false, string action = "", string id = null) @@ -190,7 +193,7 @@ private sealed class InvokeConfiguration public JsonApiMiddleware MiddleWare { get; init; } public HttpContext HttpContext { get; init; } public Mock ControllerResourceMapping { get; init; } - public Mock Options { get; init; } + public IJsonApiOptions Options { get; init; } public JsonApiRequest Request { get; init; } public Mock ResourceGraph { get; init; } } diff --git a/test/UnitTests/Middleware/JsonApiRequestTests.cs b/test/UnitTests/Middleware/JsonApiRequestTests.cs index c1b172662f..bb6a230fd7 100644 --- a/test/UnitTests/Middleware/JsonApiRequestTests.cs +++ b/test/UnitTests/Middleware/JsonApiRequestTests.cs @@ -52,6 +52,7 @@ public async Task Sets_request_properties_correctly(string requestMethod, string var graphBuilder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); graphBuilder.Add(); graphBuilder.Add(); + graphBuilder.Add(); IResourceGraph resourceGraph = graphBuilder.Build(); diff --git a/test/UnitTests/Models/RelationshipDataTests.cs b/test/UnitTests/Models/RelationshipDataTests.cs deleted file mode 100644 index 278c158028..0000000000 --- a/test/UnitTests/Models/RelationshipDataTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace UnitTests.Models -{ - public sealed class RelationshipDataTests - { - [Fact] - public void Setting_ExposeData_To_List_Sets_ManyData() - { - // Arrange - var relationshipData = new RelationshipEntry(); - - var relationships = new List - { - new() - { - Id = "9", - Type = "authors" - } - }; - - // Act - relationshipData.Data = relationships; - - // Assert - Assert.NotEmpty(relationshipData.ManyData); - Assert.Equal("authors", relationshipData.ManyData[0].Type); - Assert.Equal("9", relationshipData.ManyData[0].Id); - Assert.True(relationshipData.IsManyData); - } - - [Fact] - public void Setting_ExposeData_To_JArray_Sets_ManyData() - { - // Arrange - var relationshipData = new RelationshipEntry(); - - const string relationshipsJson = @"[ - { - ""type"": ""authors"", - ""id"": ""9"" - } - ]"; - - JArray relationships = JArray.Parse(relationshipsJson); - - // Act - relationshipData.Data = relationships; - - // Assert - Assert.NotEmpty(relationshipData.ManyData); - Assert.Equal("authors", relationshipData.ManyData[0].Type); - Assert.Equal("9", relationshipData.ManyData[0].Id); - Assert.True(relationshipData.IsManyData); - } - - [Fact] - public void Setting_ExposeData_To_RIO_Sets_SingleData() - { - // Arrange - var relationshipData = new RelationshipEntry(); - - var relationship = new ResourceIdentifierObject - { - Id = "9", - Type = "authors" - }; - - // Act - relationshipData.Data = relationship; - - // Assert - Assert.NotNull(relationshipData.SingleData); - Assert.Equal("authors", relationshipData.SingleData.Type); - Assert.Equal("9", relationshipData.SingleData.Id); - Assert.False(relationshipData.IsManyData); - } - - [Fact] - public void Setting_ExposeData_To_JObject_Sets_SingleData() - { - // Arrange - var relationshipData = new RelationshipEntry(); - - const string relationshipJson = @"{ - ""id"": ""9"", - ""type"": ""authors"" - }"; - - JObject relationship = JObject.Parse(relationshipJson); - - // Act - relationshipData.Data = relationship; - - // Assert - Assert.NotNull(relationshipData.SingleData); - Assert.Equal("authors", relationshipData.SingleData.Type); - Assert.Equal("9", relationshipData.SingleData.Id); - Assert.False(relationshipData.IsManyData); - } - } -} diff --git a/test/UnitTests/Models/ResourceConstructionTests.cs b/test/UnitTests/Models/ResourceConstructionTests.cs index 82a152dd41..81d08fe548 100644 --- a/test/UnitTests/Models/ResourceConstructionTests.cs +++ b/test/UnitTests/Models/ResourceConstructionTests.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.Design; +using System.Text.Json; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; @@ -7,7 +8,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Newtonsoft.Json; using Xunit; namespace UnitTests.Models @@ -32,13 +32,13 @@ public void When_resource_has_default_constructor_it_must_succeed() // Arrange var options = new JsonApiOptions(); - IResourceGraph graph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); + IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); var serviceContainer = new ServiceContainer(); serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor()); - var serializer = new RequestDeserializer(graph, new ResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object, - _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); + var serializer = new RequestDeserializer(resourceGraph, new ResourceFactory(serviceContainer), new TargetedFields(), + _mockHttpContextAccessor.Object, _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); var body = new { @@ -49,7 +49,7 @@ public void When_resource_has_default_constructor_it_must_succeed() } }; - string content = JsonConvert.SerializeObject(body); + string content = JsonSerializer.Serialize(body); // Act object result = serializer.Deserialize(content); @@ -65,13 +65,13 @@ public void When_resource_has_default_constructor_that_throws_it_must_fail() // Arrange var options = new JsonApiOptions(); - IResourceGraph graph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); + IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); var serviceContainer = new ServiceContainer(); serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor()); - var serializer = new RequestDeserializer(graph, new ResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object, - _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); + var serializer = new RequestDeserializer(resourceGraph, new ResourceFactory(serviceContainer), new TargetedFields(), + _mockHttpContextAccessor.Object, _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); var body = new { @@ -82,7 +82,7 @@ public void When_resource_has_default_constructor_that_throws_it_must_fail() } }; - string content = JsonConvert.SerializeObject(body); + string content = JsonSerializer.Serialize(body); // Act Action action = () => serializer.Deserialize(content); @@ -100,13 +100,13 @@ public void When_resource_has_constructor_with_string_parameter_it_must_fail() // Arrange var options = new JsonApiOptions(); - IResourceGraph graph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); + IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); var serviceContainer = new ServiceContainer(); serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor()); - var serializer = new RequestDeserializer(graph, new ResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object, - _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); + var serializer = new RequestDeserializer(resourceGraph, new ResourceFactory(serviceContainer), new TargetedFields(), + _mockHttpContextAccessor.Object, _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); var body = new { @@ -117,7 +117,7 @@ public void When_resource_has_constructor_with_string_parameter_it_must_fail() } }; - string content = JsonConvert.SerializeObject(body); + string content = JsonSerializer.Serialize(body); // Act Action action = () => serializer.Deserialize(content); diff --git a/test/UnitTests/Serialization/Client/RequestSerializerTests.cs b/test/UnitTests/Serialization/Client/RequestSerializerTests.cs deleted file mode 100644 index 96073b6645..0000000000 --- a/test/UnitTests/Serialization/Client/RequestSerializerTests.cs +++ /dev/null @@ -1,298 +0,0 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Client.Internal; -using UnitTests.TestModels; -using Xunit; - -namespace UnitTests.Serialization.Client -{ - public sealed class RequestSerializerTests : SerializerTestsSetup - { - private readonly RequestSerializer _serializer; - - public RequestSerializerTests() - { - var builder = new ResourceObjectBuilder(ResourceGraph, new ResourceObjectBuilderSettings()); - _serializer = new RequestSerializer(ResourceGraph, builder); - } - - [Fact] - public void SerializeSingle_ResourceWithDefaultTargetFields_CanBuild() - { - // Arrange - var resource = new TestResource - { - Id = 1, - StringField = "value", - NullableIntField = 123 - }; - - // Act - string serialized = _serializer.Serialize(resource); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""testResource"", - ""id"":""1"", - ""attributes"":{ - ""stringField"":""value"", - ""dateTimeField"":""0001-01-01T00:00:00"", - ""nullableDateTimeField"":null, - ""intField"":0, - ""nullableIntField"":123, - ""guidField"":""00000000-0000-0000-0000-000000000000"", - ""complexField"":null - } - } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_ResourceWithTargetedSetAttributes_CanBuild() - { - // Arrange - var resource = new TestResource - { - Id = 1, - StringField = "value", - NullableIntField = 123 - }; - - _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); - - // Act - string serialized = _serializer.Serialize(resource); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""testResource"", - ""id"":""1"", - ""attributes"":{ - ""stringField"":""value"" - } - } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_NoIdWithTargetedSetAttributes_CanBuild() - { - // Arrange - var resourceNoId = new TestResource - { - Id = 0, - StringField = "value", - NullableIntField = 123 - }; - - _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); - - // Act - string serialized = _serializer.Serialize(resourceNoId); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""testResource"", - ""attributes"":{ - ""stringField"":""value"" - } - } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_ResourceWithoutTargetedAttributes_CanBuild() - { - // Arrange - var resource = new TestResource - { - Id = 1, - StringField = "value", - NullableIntField = 123 - }; - - _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(_ => new - { - }); - - // Act - string serialized = _serializer.Serialize(resource); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""testResource"", - ""id"":""1"" - } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_ResourceWithTargetedRelationships_CanBuild() - { - // Arrange - var resourceWithRelationships = new MultipleRelationshipsPrincipalPart - { - PopulatedToOne = new OneToOneDependent - { - Id = 10 - }, - PopulatedToManies = new HashSet - { - new() - { - Id = 20 - } - } - }; - - _serializer.RelationshipsToSerialize = ResourceGraph.GetRelationships(tr => new - { - tr.EmptyToOne, - tr.EmptyToManies, - tr.PopulatedToOne, - tr.PopulatedToManies - }); - - // Act - string serialized = _serializer.Serialize(resourceWithRelationships); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""multiPrincipals"", - ""attributes"":{ - ""attributeMember"":null - }, - ""relationships"":{ - ""emptyToOne"":{ - ""data"":null - }, - ""emptyToManies"":{ - ""data"":[ ] - }, - ""populatedToOne"":{ - ""data"":{ - ""type"":""oneToOneDependents"", - ""id"":""10"" - } - }, - ""populatedToManies"":{ - ""data"":[ - { - ""type"":""oneToManyDependents"", - ""id"":""20"" - } - ] - } - } - } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeMany_ResourcesWithTargetedAttributes_CanBuild() - { - // Arrange - var resources = new List - { - new() - { - Id = 1, - StringField = "value1", - NullableIntField = 123 - }, - new() - { - Id = 2, - StringField = "value2", - NullableIntField = 123 - } - }; - - _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); - - // Act - string serialized = _serializer.Serialize(resources); - - // Assert - const string expectedFormatted = @"{ - ""data"":[ - { - ""type"":""testResource"", - ""id"":""1"", - ""attributes"":{ - ""stringField"":""value1"" - } - }, - { - ""type"":""testResource"", - ""id"":""2"", - ""attributes"":{ - ""stringField"":""value2"" - } - } - ] - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_Null_CanBuild() - { - // Arrange - _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); - - // Act - string serialized = _serializer.Serialize((IIdentifiable)null); - - // Assert - const string expectedFormatted = @"{ - ""data"":null - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeMany_EmptyList_CanBuild() - { - // Arrange - _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); - - // Act - string serialized = _serializer.Serialize(new List()); - - // Assert - const string expectedFormatted = @"{ - ""data"":[] - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - } -} diff --git a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs deleted file mode 100644 index 4c494d0b31..0000000000 --- a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs +++ /dev/null @@ -1,483 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.Design; -using System.Linq; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Client.Internal; -using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; -using UnitTests.TestModels; -using Xunit; - -namespace UnitTests.Serialization.Client -{ - public sealed class ResponseDeserializerTests : DeserializerTestsSetup - { - private readonly Dictionary _linkValues = new(); - private readonly ResponseDeserializer _deserializer; - - public ResponseDeserializerTests() - { - _deserializer = new ResponseDeserializer(ResourceGraph, new ResourceFactory(new ServiceContainer())); - _linkValues.Add("self", "http://example.com/articles"); - _linkValues.Add("next", "http://example.com/articles?page[number]=2"); - _linkValues.Add("last", "http://example.com/articles?page[number]=10"); - } - - [Fact] - public void DeserializeSingle_EmptyResponseWithMeta_CanDeserialize() - { - // Arrange - var content = new Document - { - Meta = new Dictionary - { - ["foo"] = "bar" - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - - // Assert - Assert.Null(result.Data); - Assert.NotNull(result.Meta); - Assert.Equal("bar", result.Meta["foo"]); - } - - [Fact] - public void DeserializeSingle_EmptyResponseWithTopLevelLinks_CanDeserialize() - { - // Arrange - var content = new Document - { - Links = new TopLevelLinks - { - Self = _linkValues["self"], - Next = _linkValues["next"], - Last = _linkValues["last"] - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - - // Assert - Assert.Null(result.Data); - Assert.NotNull(result.Links); - TopLevelLinks links = result.Links; - Assert.Equal(_linkValues["self"], links.Self); - Assert.Equal(_linkValues["next"], links.Next); - Assert.Equal(_linkValues["last"], links.Last); - } - - [Fact] - public void DeserializeList_EmptyResponseWithTopLevelLinks_CanDeserialize() - { - // Arrange - var content = new Document - { - Links = new TopLevelLinks - { - Self = _linkValues["self"], - Next = _linkValues["next"], - Last = _linkValues["last"] - }, - Data = new List() - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - ManyResponse result = _deserializer.DeserializeMany(body); - - // Assert - Assert.Empty(result.Data); - Assert.NotNull(result.Links); - TopLevelLinks links = result.Links; - Assert.Equal(_linkValues["self"], links.Self); - Assert.Equal(_linkValues["next"], links.Next); - Assert.Equal(_linkValues["last"], links.Last); - } - - [Fact] - public void DeserializeSingle_ResourceWithAttributes_CanDeserialize() - { - // Arrange - Document content = CreateTestResourceDocument(); - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - TestResource resource = result.Data; - - // Assert - Assert.Null(result.Links); - Assert.Null(result.Meta); - Assert.Equal(1, resource.Id); - Assert.Equal(content.SingleData.Attributes["stringField"], resource.StringField); - } - - [Fact] - public void DeserializeSingle_MultipleDependentRelationshipsWithIncluded_CanDeserialize() - { - // Arrange - Document content = CreateDocumentWithRelationships("multiPrincipals"); - content.SingleData.Relationships.Add("populatedToOne", CreateRelationshipData("oneToOneDependents")); - content.SingleData.Relationships.Add("populatedToManies", CreateRelationshipData("oneToManyDependents", true)); - content.SingleData.Relationships.Add("emptyToOne", CreateRelationshipData()); - content.SingleData.Relationships.Add("emptyToManies", CreateRelationshipData(isToManyData: true)); - const string toOneAttributeValue = "populatedToOne member content"; - const string toManyAttributeValue = "populatedToManies member content"; - - content.Included = new List - { - new() - { - Type = "oneToOneDependents", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = toOneAttributeValue - } - }, - new() - { - Type = "oneToManyDependents", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = toManyAttributeValue - } - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - MultipleRelationshipsPrincipalPart resource = result.Data; - - // Assert - Assert.Equal(1, resource.Id); - Assert.NotNull(resource.PopulatedToOne); - Assert.Equal(toOneAttributeValue, resource.PopulatedToOne.AttributeMember); - Assert.Equal(toManyAttributeValue, resource.PopulatedToManies.First().AttributeMember); - Assert.NotNull(resource.PopulatedToManies); - Assert.NotNull(resource.EmptyToManies); - Assert.Empty(resource.EmptyToManies); - Assert.Null(resource.EmptyToOne); - } - - [Fact] - public void DeserializeSingle_MultiplePrincipalRelationshipsWithIncluded_CanDeserialize() - { - // Arrange - Document content = CreateDocumentWithRelationships("multiDependents"); - content.SingleData.Relationships.Add("populatedToOne", CreateRelationshipData("oneToOnePrincipals")); - content.SingleData.Relationships.Add("populatedToMany", CreateRelationshipData("oneToManyPrincipals")); - content.SingleData.Relationships.Add("emptyToOne", CreateRelationshipData()); - content.SingleData.Relationships.Add("emptyToMany", CreateRelationshipData()); - const string toOneAttributeValue = "populatedToOne member content"; - const string toManyAttributeValue = "populatedToManies member content"; - - content.Included = new List - { - new() - { - Type = "oneToOnePrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = toOneAttributeValue - } - }, - new() - { - Type = "oneToManyPrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = toManyAttributeValue - } - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - MultipleRelationshipsDependentPart resource = result.Data; - - // Assert - Assert.Equal(1, resource.Id); - Assert.NotNull(resource.PopulatedToOne); - Assert.Equal(toOneAttributeValue, resource.PopulatedToOne.AttributeMember); - Assert.Equal(toManyAttributeValue, resource.PopulatedToMany.AttributeMember); - Assert.NotNull(resource.PopulatedToMany); - Assert.Null(resource.EmptyToMany); - Assert.Null(resource.EmptyToOne); - } - - [Fact] - public void DeserializeSingle_NestedIncluded_CanDeserialize() - { - // Arrange - Document content = CreateDocumentWithRelationships("multiPrincipals"); - content.SingleData.Relationships.Add("populatedToManies", CreateRelationshipData("oneToManyDependents", true)); - const string toManyAttributeValue = "populatedToManies member content"; - const string nestedIncludeAttributeValue = "nested include member content"; - - content.Included = new List - { - new() - { - Type = "oneToManyDependents", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = toManyAttributeValue - }, - Relationships = new Dictionary - { - ["principal"] = CreateRelationshipData("oneToManyPrincipals") - } - }, - new() - { - Type = "oneToManyPrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = nestedIncludeAttributeValue - } - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - MultipleRelationshipsPrincipalPart resource = result.Data; - - // Assert - Assert.Equal(1, resource.Id); - Assert.Null(resource.PopulatedToOne); - Assert.Null(resource.EmptyToManies); - Assert.Null(resource.EmptyToOne); - Assert.NotNull(resource.PopulatedToManies); - OneToManyDependent includedResource = resource.PopulatedToManies.First(); - Assert.Equal(toManyAttributeValue, includedResource.AttributeMember); - OneToManyPrincipal nestedIncludedResource = includedResource.Principal; - Assert.Equal(nestedIncludeAttributeValue, nestedIncludedResource.AttributeMember); - } - - [Fact] - public void DeserializeSingle_DeeplyNestedIncluded_CanDeserialize() - { - // Arrange - Document content = CreateDocumentWithRelationships("multiPrincipals"); - content.SingleData.Relationships.Add("multi", CreateRelationshipData("multiPrincipals")); - const string includedAttributeValue = "multi member content"; - const string nestedIncludedAttributeValue = "nested include member content"; - const string deeplyNestedIncludedAttributeValue = "deeply nested member content"; - - content.Included = new List - { - new() - { - Type = "multiPrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = includedAttributeValue - }, - Relationships = new Dictionary - { - ["populatedToManies"] = CreateRelationshipData("oneToManyDependents", true) - } - }, - new() - { - Type = "oneToManyDependents", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = nestedIncludedAttributeValue - }, - Relationships = new Dictionary - { - ["principal"] = CreateRelationshipData("oneToManyPrincipals") - } - }, - new() - { - Type = "oneToManyPrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = deeplyNestedIncludedAttributeValue - } - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - MultipleRelationshipsPrincipalPart resource = result.Data; - - // Assert - Assert.Equal(1, resource.Id); - MultipleRelationshipsPrincipalPart included = resource.Multi; - Assert.Equal(10, included.Id); - Assert.Equal(includedAttributeValue, included.AttributeMember); - OneToManyDependent nestedIncluded = included.PopulatedToManies.First(); - Assert.Equal(10, nestedIncluded.Id); - Assert.Equal(nestedIncludedAttributeValue, nestedIncluded.AttributeMember); - OneToManyPrincipal deeplyNestedIncluded = nestedIncluded.Principal; - Assert.Equal(10, deeplyNestedIncluded.Id); - Assert.Equal(deeplyNestedIncludedAttributeValue, deeplyNestedIncluded.AttributeMember); - } - - [Fact] - public void DeserializeList_DeeplyNestedIncluded_CanDeserialize() - { - // Arrange - var content = new Document - { - Data = new List - { - CreateDocumentWithRelationships("multiPrincipals").SingleData - } - }; - - content.ManyData[0].Relationships.Add("multi", CreateRelationshipData("multiPrincipals")); - const string includedAttributeValue = "multi member content"; - const string nestedIncludedAttributeValue = "nested include member content"; - const string deeplyNestedIncludedAttributeValue = "deeply nested member content"; - - content.Included = new List - { - new() - { - Type = "multiPrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = includedAttributeValue - }, - Relationships = new Dictionary - { - ["populatedToManies"] = CreateRelationshipData("oneToManyDependents", true) - } - }, - new() - { - Type = "oneToManyDependents", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = nestedIncludedAttributeValue - }, - Relationships = new Dictionary - { - ["principal"] = CreateRelationshipData("oneToManyPrincipals") - } - }, - new() - { - Type = "oneToManyPrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = deeplyNestedIncludedAttributeValue - } - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - ManyResponse result = _deserializer.DeserializeMany(body); - MultipleRelationshipsPrincipalPart resource = result.Data.First(); - - // Assert - Assert.Equal(1, resource.Id); - MultipleRelationshipsPrincipalPart included = resource.Multi; - Assert.Equal(10, included.Id); - Assert.Equal(includedAttributeValue, included.AttributeMember); - OneToManyDependent nestedIncluded = included.PopulatedToManies.First(); - Assert.Equal(10, nestedIncluded.Id); - Assert.Equal(nestedIncludedAttributeValue, nestedIncluded.AttributeMember); - OneToManyPrincipal deeplyNestedIncluded = nestedIncluded.Principal; - Assert.Equal(10, deeplyNestedIncluded.Id); - Assert.Equal(deeplyNestedIncludedAttributeValue, deeplyNestedIncluded.AttributeMember); - } - - [Fact] - public void DeserializeSingle_ResourceWithInheritanceAndInclusions_CanDeserialize() - { - // Arrange - Document content = CreateDocumentWithRelationships("testResourceWithAbstractRelationships"); - content.SingleData.Relationships.Add("toMany", CreateRelationshipData("firstDerivedModels", true)); - content.SingleData.Relationships["toMany"].ManyData.Add(CreateRelationshipData("secondDerivedModels", id: "11").SingleData); - content.SingleData.Relationships.Add("toOne", CreateRelationshipData("firstDerivedModels", id: "20")); - - content.Included = new List - { - new() - { - Type = "firstDerivedModels", - Id = "10", - Attributes = new Dictionary - { - ["firstProperty"] = "true" - } - }, - new() - { - Type = "secondDerivedModels", - Id = "11", - Attributes = new Dictionary - { - ["secondProperty"] = "false" - } - }, - new() - { - Type = "firstDerivedModels", - Id = "20", - Attributes = new Dictionary - { - ["firstProperty"] = "true" - } - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - TestResourceWithAbstractRelationship resource = result.Data; - - // Assert - Assert.Equal(1, resource.Id); - Assert.NotNull(resource.ToOne); - Assert.True(resource.ToOne is FirstDerivedModel); - Assert.True(((FirstDerivedModel)resource.ToOne).FirstProperty); - - Assert.NotEmpty(resource.ToMany); - BaseModel first = resource.ToMany[0]; - Assert.True(first is FirstDerivedModel); - Assert.True(((FirstDerivedModel)first).FirstProperty); - - BaseModel second = resource.ToMany[1]; - Assert.True(second is SecondDerivedModel); - Assert.False(((SecondDerivedModel)second).SecondProperty); - } - } -} diff --git a/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs b/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs index e27938e1d3..5e590a4e21 100644 --- a/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs +++ b/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs @@ -31,8 +31,8 @@ public void ResourceToDocument_NullResource_CanBuild() Document document = _builder.PublicBuild((TestResource)null); // Assert - Assert.Null(document.Data); - Assert.False(document.IsPopulated); + Assert.Null(document.Data.Value); + Assert.True(document.Data.IsAssigned); } [Fact] @@ -42,8 +42,8 @@ public void ResourceToDocument_EmptyList_CanBuild() Document document = _builder.PublicBuild(new List()); // Assert - Assert.NotNull(document.Data); - Assert.Empty(document.ManyData); + Assert.NotNull(document.Data.Value); + Assert.Empty(document.Data.ManyValue); } [Fact] @@ -56,8 +56,8 @@ public void ResourceToDocument_SingleResource_CanBuild() Document document = _builder.PublicBuild(dummy); // Assert - Assert.NotNull(document.Data); - Assert.True(document.IsPopulated); + Assert.NotNull(document.Data.Value); + Assert.True(document.Data.IsAssigned); } [Fact] @@ -68,7 +68,7 @@ public void ResourceToDocument_ResourceList_CanBuild() // Act Document document = _builder.PublicBuild(resources); - var data = (List)document.Data; + IList data = document.Data.ManyValue; // Assert Assert.Equal(2, data.Count); diff --git a/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs b/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs index a87890afdf..6e6b417af9 100644 --- a/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs +++ b/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs @@ -3,10 +3,10 @@ using System.ComponentModel.Design; using System.Linq; using System.Reflection; +using System.Text.Json; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; using UnitTests.TestModels; using Xunit; @@ -18,7 +18,7 @@ public sealed class BaseDocumentParserTests : DeserializerTestsSetup public BaseDocumentParserTests() { - _deserializer = new TestDeserializer(ResourceGraph, new ResourceFactory(new ServiceContainer())); + _deserializer = new TestDeserializer(ResourceGraph, new ResourceFactory(new ServiceContainer()), Options); } [Fact] @@ -27,14 +27,14 @@ public void DeserializeResourceIdentifiers_SingleData_CanDeserialize() // Arrange var content = new Document { - Data = new ResourceObject + Data = new SingleOrManyData(new ResourceObject { Type = "testResource", Id = "1" - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (TestResource)_deserializer.Deserialize(body); @@ -48,7 +48,7 @@ public void DeserializeResourceIdentifiers_EmptySingleData_CanDeserialize() { // Arrange var content = new Document(); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act object result = _deserializer.Deserialize(body); @@ -63,17 +63,17 @@ public void DeserializeResourceIdentifiers_ArrayData_CanDeserialize() // Arrange var content = new Document { - Data = new List + Data = new SingleOrManyData(new List { new() { Type = "testResource", Id = "1" } - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (IEnumerable)_deserializer.Deserialize(body); @@ -87,10 +87,10 @@ public void DeserializeResourceIdentifiers_EmptyArrayData_CanDeserialize() { var content = new Document { - Data = new List() + Data = new SingleOrManyData(Array.Empty()) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (IEnumerable)_deserializer.Deserialize(body); @@ -104,12 +104,12 @@ public void DeserializeResourceIdentifiers_EmptyArrayData_CanDeserialize() [InlineData("stringField", null)] [InlineData("intField", null, true)] [InlineData("intField", 1)] - [InlineData("intField", "1")] + [InlineData("intField", "1", true)] [InlineData("nullableIntField", null)] - [InlineData("nullableIntField", "1")] + [InlineData("nullableIntField", 1)] [InlineData("guidField", "bad format", true)] [InlineData("guidField", "1a68be43-cc84-4924-a421-7f4d614b7781")] - [InlineData("dateTimeField", "9/11/2019 11:41:40 AM")] + [InlineData("dateTimeField", "9/11/2019 11:41:40 AM", true)] [InlineData("dateTimeField", null, true)] [InlineData("nullableDateTimeField", null)] public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, object value, bool expectError = false) @@ -117,7 +117,7 @@ public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, // Arrange var content = new Document { - Data = new ResourceObject + Data = new SingleOrManyData(new ResourceObject { Type = "testResource", Id = "1", @@ -125,10 +125,10 @@ public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, { [member] = value } - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act Func action = () => (TestResource)_deserializer.Deserialize(body); @@ -136,13 +136,13 @@ public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, // Assert if (expectError) { - Assert.ThrowsAny(action); + Assert.ThrowsAny(action); } else { TestResource resource = action(); - PropertyInfo pi = ResourceGraph.GetResourceContext("testResource").Attributes.Single(attr => attr.PublicName == member).Property; + PropertyInfo pi = ResourceGraph.GetResourceContext("testResource").GetAttributeByPublicName(member).Property; object deserializedValue = pi.GetValue(resource); if (member == "intField") @@ -153,7 +153,7 @@ public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, { Assert.Null(deserializedValue); } - else if (member == "nullableIntField" && (string)value == "1") + else if (member == "nullableIntField" && (int?)value == 1) { Assert.Equal(1, deserializedValue); } @@ -178,7 +178,7 @@ public void DeserializeAttributes_ComplexType_CanDeserialize() // Arrange var content = new Document { - Data = new ResourceObject + Data = new SingleOrManyData(new ResourceObject { Type = "testResource", Id = "1", @@ -189,10 +189,10 @@ public void DeserializeAttributes_ComplexType_CanDeserialize() ["compoundName"] = "testName" } } - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (TestResource)_deserializer.Deserialize(body); @@ -208,7 +208,7 @@ public void DeserializeAttributes_ComplexListType_CanDeserialize() // Arrange var content = new Document { - Data = new ResourceObject + Data = new SingleOrManyData(new ResourceObject { Type = "testResource-with-list", Id = "1", @@ -222,10 +222,10 @@ public void DeserializeAttributes_ComplexListType_CanDeserialize() } } } - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (TestResourceWithList)_deserializer.Deserialize(body); @@ -242,16 +242,16 @@ public void DeserializeRelationship_SingleDataForToOneRelationship_CannotDeseria // Arrange Document content = CreateDocumentWithRelationships("oneToManyPrincipals", "dependents"); - content.SingleData.Relationships["dependents"] = new RelationshipEntry + content.Data.SingleValue.Relationships["dependents"] = new RelationshipObject { - Data = new ResourceIdentifierObject + Data = new SingleOrManyData(new ResourceIdentifierObject { Type = "Dependents", Id = "1" - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act Action action = () => _deserializer.Deserialize(body); @@ -266,19 +266,19 @@ public void DeserializeRelationship_ManyDataForToManyRelationship_CannotDeserial // Arrange Document content = CreateDocumentWithRelationships("oneToOnePrincipals", "dependent"); - content.SingleData.Relationships["dependent"] = new RelationshipEntry + content.Data.SingleValue.Relationships["dependent"] = new RelationshipObject { - Data = new List + Data = new SingleOrManyData(new List { new() { Type = "Dependent", Id = "1" } - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act Action action = () => _deserializer.Deserialize(body); @@ -292,7 +292,7 @@ public void DeserializeRelationships_EmptyOneToOneDependent_NavigationPropertyIs { // Arrange Document content = CreateDocumentWithRelationships("oneToOnePrincipals", "dependent"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToOnePrincipal)_deserializer.Deserialize(body); @@ -308,7 +308,7 @@ public void DeserializeRelationships_PopulatedOneToOneDependent_NavigationProper { // Arrange Document content = CreateDocumentWithRelationships("oneToOnePrincipals", "dependent", "oneToOneDependents"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToOnePrincipal)_deserializer.Deserialize(body); @@ -324,7 +324,7 @@ public void DeserializeRelationships_EmptyOneToOnePrincipal_NavigationIsNull() { // Arrange Document content = CreateDocumentWithRelationships("oneToOneDependents", "principal"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToOneDependent)_deserializer.Deserialize(body); @@ -339,7 +339,7 @@ public void DeserializeRelationships_EmptyRequiredOneToOnePrincipal_NavigationIs { // Arrange Document content = CreateDocumentWithRelationships("oneToOneRequiredDependents", "principal"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToOneRequiredDependent)_deserializer.Deserialize(body); @@ -354,7 +354,7 @@ public void DeserializeRelationships_PopulatedOneToOnePrincipal_NavigationIsPopu { // Arrange Document content = CreateDocumentWithRelationships("oneToOneDependents", "principal", "oneToOnePrincipals"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToOneDependent)_deserializer.Deserialize(body); @@ -371,7 +371,7 @@ public void DeserializeRelationships_EmptyOneToManyPrincipal_NavigationIsNull() { // Arrange Document content = CreateDocumentWithRelationships("oneToManyDependents", "principal"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToManyDependent)_deserializer.Deserialize(body); @@ -387,7 +387,7 @@ public void DeserializeRelationships_EmptyOneToManyRequiredPrincipal_NavigationI { // Arrange Document content = CreateDocumentWithRelationships("oneToMany-requiredDependents", "principal"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToManyRequiredDependent)_deserializer.Deserialize(body); @@ -403,7 +403,7 @@ public void DeserializeRelationships_PopulatedOneToManyPrincipal_NavigationIsPop { // Arrange Document content = CreateDocumentWithRelationships("oneToManyDependents", "principal", "oneToManyPrincipals"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToManyDependent)_deserializer.Deserialize(body); @@ -420,7 +420,7 @@ public void DeserializeRelationships_EmptyOneToManyDependent_NavigationIsNull() { // Arrange Document content = CreateDocumentWithRelationships("oneToManyPrincipals", "dependents", isToManyData: true); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToManyPrincipal)_deserializer.Deserialize(body); @@ -436,7 +436,7 @@ public void DeserializeRelationships_PopulatedOneToManyDependent_NavigationIsPop { // Arrange Document content = CreateDocumentWithRelationships("oneToManyPrincipals", "dependents", "oneToManyDependents", true); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToManyPrincipal)_deserializer.Deserialize(body); diff --git a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs index 61872c4945..3e44774876 100644 --- a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs @@ -1,6 +1,6 @@ -using System.Collections; using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; @@ -15,7 +15,7 @@ public sealed class ResourceObjectBuilderTests : SerializerTestsSetup public ResourceObjectBuilderTests() { - _builder = new ResourceObjectBuilder(ResourceGraph, new ResourceObjectBuilderSettings()); + _builder = new ResourceObjectBuilder(ResourceGraph, new JsonApiOptions()); } [Fact] @@ -151,16 +151,16 @@ public void ResourceWithRelationshipsToResourceObject_WithIncludedRelationshipsA // Assert Assert.Equal(4, resourceObject.Relationships.Count); - Assert.Null(resourceObject.Relationships["emptyToOne"].Data); - Assert.Empty((IList)resourceObject.Relationships["emptyToManies"].Data); - var populatedToOneData = (ResourceIdentifierObject)resourceObject.Relationships["populatedToOne"].Data; + Assert.Null(resourceObject.Relationships["emptyToOne"].Data.SingleValue); + Assert.Empty(resourceObject.Relationships["emptyToManies"].Data.ManyValue); + ResourceIdentifierObject populatedToOneData = resourceObject.Relationships["populatedToOne"].Data.SingleValue; Assert.NotNull(populatedToOneData); Assert.Equal("10", populatedToOneData.Id); Assert.Equal("oneToOneDependents", populatedToOneData.Type); - var populatedToManiesData = (List)resourceObject.Relationships["populatedToManies"].Data; - Assert.Single(populatedToManiesData); - Assert.Equal("20", populatedToManiesData.First().Id); - Assert.Equal("oneToManyDependents", populatedToManiesData.First().Type); + IList populatedToManyData = resourceObject.Relationships["populatedToManies"].Data.ManyValue; + Assert.Single(populatedToManyData); + Assert.Equal("20", populatedToManyData.First().Id); + Assert.Equal("oneToManyDependents", populatedToManyData.First().Type); } [Fact] @@ -183,8 +183,8 @@ public void ResourceWithRelationshipsToResourceObject_DeviatingForeignKeyWhileRe // Assert Assert.Single(resourceObject.Relationships); - Assert.NotNull(resourceObject.Relationships["principal"].Data); - var ro = (ResourceIdentifierObject)resourceObject.Relationships["principal"].Data; + Assert.NotNull(resourceObject.Relationships["principal"].Data.Value); + ResourceIdentifierObject ro = resourceObject.Relationships["principal"].Data.SingleValue; Assert.Equal("10", ro.Id); } @@ -204,7 +204,7 @@ public void ResourceWithRelationshipsToResourceObject_DeviatingForeignKeyAndNoNa ResourceObject resourceObject = _builder.Build(resource, relationships: relationships); // Assert - Assert.Null(resourceObject.Relationships["principal"].Data); + Assert.Null(resourceObject.Relationships["principal"].Data.Value); } [Fact] @@ -227,8 +227,8 @@ public void ResourceWithRequiredRelationshipsToResourceObject_DeviatingForeignKe // Assert Assert.Single(resourceObject.Relationships); - Assert.NotNull(resourceObject.Relationships["principal"].Data); - var ro = (ResourceIdentifierObject)resourceObject.Relationships["principal"].Data; + Assert.NotNull(resourceObject.Relationships["principal"].Data.SingleValue); + ResourceIdentifierObject ro = resourceObject.Relationships["principal"].Data.SingleValue; Assert.Equal("10", ro.Id); } } diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs index bf851f0f0c..05fc33c886 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -1,9 +1,13 @@ +using System; using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.JsonConverters; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; using Moq; @@ -12,10 +16,16 @@ namespace UnitTests.Serialization { public class DeserializerTestsSetup : SerializationTestsSetupBase { + protected readonly JsonApiOptions Options = new(); + protected readonly JsonSerializerOptions SerializerWriteOptions; + protected Mock MockHttpContextAccessor { get; } protected DeserializerTestsSetup() { + Options.SerializerOptions.Converters.Add(new ResourceObjectConverter(ResourceGraph)); + + SerializerWriteOptions = ((IJsonApiOptions)Options).SerializerWriteOptions; MockHttpContextAccessor = new Mock(); MockHttpContextAccessor.Setup(mock => mock.HttpContext).Returns(new DefaultHttpContext()); } @@ -24,7 +34,7 @@ protected Document CreateDocumentWithRelationships(string primaryType, string re bool isToManyData = false) { Document content = CreateDocumentWithRelationships(primaryType); - content.SingleData.Relationships.Add(relationshipMemberName, CreateRelationshipData(relatedType, isToManyData)); + content.Data.SingleValue.Relationships.Add(relationshipMemberName, CreateRelationshipData(relatedType, isToManyData)); return content; } @@ -32,18 +42,18 @@ protected Document CreateDocumentWithRelationships(string primaryType) { return new() { - Data = new ResourceObject + Data = new SingleOrManyData(new ResourceObject { Id = "1", Type = primaryType, - Relationships = new Dictionary() - } + Relationships = new Dictionary() + }) }; } - protected RelationshipEntry CreateRelationshipData(string relatedType = null, bool isToManyData = false, string id = "10") + protected RelationshipObject CreateRelationshipData(string relatedType = null, bool isToManyData = false, string id = "10") { - var entry = new RelationshipEntry(); + var relationshipObject = new RelationshipObject(); ResourceIdentifierObject rio = relatedType == null ? null @@ -55,21 +65,22 @@ protected RelationshipEntry CreateRelationshipData(string relatedType = null, bo if (isToManyData) { - entry.Data = relatedType != null ? rio.AsList() : new List(); + IList rios = relatedType != null ? rio.AsList() : Array.Empty(); + relationshipObject.Data = new SingleOrManyData(rios); } else { - entry.Data = rio; + relationshipObject.Data = new SingleOrManyData(rio); } - return entry; + return relationshipObject; } protected Document CreateTestResourceDocument() { return new() { - Data = new ResourceObject + Data = new SingleOrManyData(new ResourceObject { Type = "testResource", Id = "1", @@ -79,25 +90,28 @@ protected Document CreateTestResourceDocument() ["intField"] = 1, ["nullableIntField"] = null, ["guidField"] = "1a68be43-cc84-4924-a421-7f4d614b7781", - ["dateTimeField"] = "9/11/2019 11:41:40 AM" + ["dateTimeField"] = DateTime.Parse("9/11/2019 11:41:40 AM", CultureInfo.InvariantCulture) } - } + }) }; } protected sealed class TestDeserializer : BaseDeserializer { - public TestDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory) + private readonly IJsonApiOptions _options; + + public TestDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory, IJsonApiOptions options) : base(resourceGraph, resourceFactory) { + _options = options; } public object Deserialize(string body) { - return DeserializeBody(body); + return DeserializeData(body, _options.SerializerReadOptions); } - protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) + protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipObject data = null) { } } diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index 08ff89ad21..ccc5dae8e9 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -61,15 +61,13 @@ protected ResponseSerializer GetResponseSerializer(IEnumerable(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, resourceDefinitionAccessor, jsonApiOptions); + return new ResponseSerializer(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, resourceDefinitionAccessor, options); } protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(IEnumerable> inclusionChains = null, @@ -81,9 +79,10 @@ protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(IEnumer IEnumerable includeConstraints = GetIncludeConstraints(inclusionChainArray); IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(inclusionChains != null); IEvaluatedIncludeCache evaluatedIncludeCache = GetEvaluatedIncludeCache(inclusionChainArray); + var options = new JsonApiOptions(); - return new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, GetResourceDefinitionAccessor(), - GetSerializerSettingsProvider(), evaluatedIncludeCache); + return new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, GetResourceDefinitionAccessor(), options, + evaluatedIncludeCache); } private IIncludedResourceObjectBuilder GetIncludedBuilder(bool hasIncludeQueryString) @@ -92,17 +91,10 @@ private IIncludedResourceObjectBuilder GetIncludedBuilder(bool hasIncludeQuerySt ILinkBuilder linkBuilder = GetLinkBuilder(); IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor(); IRequestQueryStringAccessor queryStringAccessor = new FakeRequestQueryStringAccessor(hasIncludeQueryString ? "include=" : null); - IResourceObjectBuilderSettingsProvider resourceObjectBuilderSettingsProvider = GetSerializerSettingsProvider(); + var options = new JsonApiOptions(); return new IncludedResourceObjectBuilder(fieldsToSerialize, linkBuilder, ResourceGraph, Enumerable.Empty(), - resourceDefinitionAccessor, queryStringAccessor, resourceObjectBuilderSettingsProvider); - } - - protected IResourceObjectBuilderSettingsProvider GetSerializerSettingsProvider() - { - var mock = new Mock(); - mock.Setup(provider => provider.Get()).Returns(new ResourceObjectBuilderSettings()); - return mock.Object; + resourceDefinitionAccessor, queryStringAccessor, options); } private IResourceDefinitionAccessor GetResourceDefinitionAccessor() diff --git a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs index ff9a2d5855..ee4fb88492 100644 --- a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs @@ -31,11 +31,11 @@ public void BuildIncluded_DeeplyNestedCircularChainOfSingleData_CanBuild() Assert.Equal(6, result.Count); ResourceObject authorResourceObject = result.Single(ro => ro.Type == "people" && ro.Id == author.StringId); - ResourceIdentifierObject authorFoodRelation = authorResourceObject.Relationships["favoriteFood"].SingleData; + ResourceIdentifierObject authorFoodRelation = authorResourceObject.Relationships["favoriteFood"].Data.SingleValue; Assert.Equal(author.FavoriteFood.StringId, authorFoodRelation.Id); ResourceObject reviewerResourceObject = result.Single(ro => ro.Type == "people" && ro.Id == reviewer.StringId); - ResourceIdentifierObject reviewerFoodRelation = reviewerResourceObject.Relationships["favoriteFood"].SingleData; + ResourceIdentifierObject reviewerFoodRelation = reviewerResourceObject.Relationships["favoriteFood"].Data.SingleValue; Assert.Equal(reviewer.FavoriteFood.StringId, reviewerFoodRelation.Id); } @@ -111,7 +111,7 @@ public void BuildIncluded_DuplicateChildrenMultipleChains_OnceInOutput() IList result = builder.Build(); Assert.Single(result); Assert.Equal(person.Name, result[0].Attributes["name"]); - Assert.Equal(person.Id.ToString(), result[0].Id); + Assert.Equal(person.StringId, result[0].Id); } private Song GetReviewerChainInstances(Article article, Blog sharedBlog, Person sharedBlogAuthor) @@ -166,8 +166,7 @@ private IReadOnlyCollection GetIncludedRelationshipsChain foreach (string requestedRelationship in splitPath) { - RelationshipAttribute relationship = - resourceContext.Relationships.Single(nextRelationship => nextRelationship.PublicName == requestedRelationship); + RelationshipAttribute relationship = resourceContext.GetRelationshipByPublicName(requestedRelationship); parsedChain.Add(relationship); resourceContext = ResourceGraph.GetResourceContext(relationship.RightType); @@ -182,10 +181,10 @@ private IncludedResourceObjectBuilder GetBuilder() ILinkBuilder links = GetLinkBuilder(); IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock().Object; var queryStringAccessor = new FakeRequestQueryStringAccessor(); - IResourceObjectBuilderSettingsProvider resourceObjectBuilderSettingsProvider = GetSerializerSettingsProvider(); + var options = new JsonApiOptions(); return new IncludedResourceObjectBuilder(fields, links, ResourceGraph, Enumerable.Empty(), resourceDefinitionAccessor, - queryStringAccessor, resourceObjectBuilderSettingsProvider); + queryStringAccessor, options); } private sealed class AuthorChainInstances diff --git a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs index e683e67c02..c6f8747eba 100644 --- a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs +++ b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs @@ -1,12 +1,13 @@ using System.Collections.Generic; +using System.Text.Json; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.JsonConverters; using JsonApiDotNetCore.Serialization.Objects; using Moq; -using Newtonsoft.Json; using Xunit; namespace UnitTests.Serialization.Server @@ -20,8 +21,11 @@ public sealed class RequestDeserializerTests : DeserializerTestsSetup public RequestDeserializerTests() { + var options = new JsonApiOptions(); + options.SerializerOptions.Converters.Add(new ResourceObjectConverter(ResourceGraph)); + _deserializer = new RequestDeserializer(ResourceGraph, new TestResourceFactory(), _fieldsManagerMock.Object, MockHttpContextAccessor.Object, - _requestMock.Object, new JsonApiOptions(), _resourceDefinitionAccessorMock.Object); + _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); } [Fact] @@ -33,7 +37,8 @@ public void DeserializeAttributes_VariousUpdatedMembers_RegistersTargetedFields( SetupFieldsManager(attributesToUpdate, relationshipsToUpdate); Document content = CreateTestResourceDocument(); - string body = JsonConvert.SerializeObject(content); + + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act _deserializer.Deserialize(body); @@ -52,11 +57,12 @@ public void DeserializeRelationships_MultipleDependentRelationships_RegistersUpd SetupFieldsManager(attributesToUpdate, relationshipsToUpdate); Document content = CreateDocumentWithRelationships("multiPrincipals"); - content.SingleData.Relationships.Add("populatedToOne", CreateRelationshipData("oneToOneDependents")); - content.SingleData.Relationships.Add("emptyToOne", CreateRelationshipData()); - content.SingleData.Relationships.Add("populatedToManies", CreateRelationshipData("oneToManyDependents", true)); - content.SingleData.Relationships.Add("emptyToManies", CreateRelationshipData(isToManyData: true)); - string body = JsonConvert.SerializeObject(content); + content.Data.SingleValue.Relationships.Add("populatedToOne", CreateRelationshipData("oneToOneDependents")); + content.Data.SingleValue.Relationships.Add("emptyToOne", CreateRelationshipData()); + content.Data.SingleValue.Relationships.Add("populatedToManies", CreateRelationshipData("oneToManyDependents", true)); + content.Data.SingleValue.Relationships.Add("emptyToManies", CreateRelationshipData(isToManyData: true)); + + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act _deserializer.Deserialize(body); @@ -75,11 +81,12 @@ public void DeserializeRelationships_MultiplePrincipalRelationships_RegistersUpd SetupFieldsManager(attributesToUpdate, relationshipsToUpdate); Document content = CreateDocumentWithRelationships("multiDependents"); - content.SingleData.Relationships.Add("populatedToOne", CreateRelationshipData("oneToOnePrincipals")); - content.SingleData.Relationships.Add("emptyToOne", CreateRelationshipData()); - content.SingleData.Relationships.Add("populatedToMany", CreateRelationshipData("oneToManyPrincipals")); - content.SingleData.Relationships.Add("emptyToMany", CreateRelationshipData()); - string body = JsonConvert.SerializeObject(content); + content.Data.SingleValue.Relationships.Add("populatedToOne", CreateRelationshipData("oneToOnePrincipals")); + content.Data.SingleValue.Relationships.Add("emptyToOne", CreateRelationshipData()); + content.Data.SingleValue.Relationships.Add("populatedToMany", CreateRelationshipData("oneToManyPrincipals")); + content.Data.SingleValue.Relationships.Add("emptyToMany", CreateRelationshipData()); + + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act _deserializer.Deserialize(body); diff --git a/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs index 183b23ed9b..e103cac808 100644 --- a/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs @@ -23,7 +23,7 @@ public ResponseResourceObjectBuilderTests() } [Fact] - public void Build_RelationshipNotIncludedAndLinksEnabled_RelationshipEntryWithLinks() + public void Build_RelationshipNotIncludedAndLinksEnabled_RelationshipObjectWithLinks() { // Arrange var resource = new OneToManyPrincipal @@ -37,10 +37,10 @@ public void Build_RelationshipNotIncludedAndLinksEnabled_RelationshipEntryWithLi ResourceObject resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); // Assert - Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out RelationshipEntry entry)); - Assert.Equal("http://www.dummy.com/dummy-relationship-self-link", entry.Links.Self); - Assert.Equal("http://www.dummy.com/dummy-relationship-related-link", entry.Links.Related); - Assert.False(entry.IsPopulated); + Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out RelationshipObject relationshipObject)); + Assert.Equal("http://www.dummy.com/dummy-relationship-self-link", relationshipObject.Links.Self); + Assert.Equal("http://www.dummy.com/dummy-relationship-related-link", relationshipObject.Links.Related); + Assert.False(relationshipObject.Data.IsAssigned); } [Fact] @@ -62,7 +62,7 @@ public void Build_RelationshipNotIncludedAndLinksDisabled_NoRelationshipObject() } [Fact] - public void Build_RelationshipIncludedAndLinksDisabled_RelationshipEntryWithData() + public void Build_RelationshipIncludedAndLinksDisabled_RelationshipObjectWithData() { // Arrange var resource = new OneToManyPrincipal @@ -83,14 +83,14 @@ public void Build_RelationshipIncludedAndLinksDisabled_RelationshipEntryWithData ResourceObject resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); // Assert - Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out RelationshipEntry entry)); - Assert.Null(entry.Links); - Assert.True(entry.IsPopulated); - Assert.Equal("20", entry.ManyData.Single().Id); + Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out RelationshipObject relationshipObject)); + Assert.Null(relationshipObject.Links); + Assert.True(relationshipObject.Data.IsAssigned); + Assert.Equal("20", relationshipObject.Data.ManyValue.Single().Id); } [Fact] - public void Build_RelationshipIncludedAndLinksEnabled_RelationshipEntryWithDataAndLinks() + public void Build_RelationshipIncludedAndLinksEnabled_RelationshipObjectWithDataAndLinks() { // Arrange var resource = new OneToManyPrincipal @@ -112,11 +112,11 @@ public void Build_RelationshipIncludedAndLinksEnabled_RelationshipEntryWithDataA ResourceObject resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); // Assert - Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out RelationshipEntry entry)); - Assert.Equal("http://www.dummy.com/dummy-relationship-self-link", entry.Links.Self); - Assert.Equal("http://www.dummy.com/dummy-relationship-related-link", entry.Links.Related); - Assert.True(entry.IsPopulated); - Assert.Equal("20", entry.ManyData.Single().Id); + Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out RelationshipObject relationshipObject)); + Assert.Equal("http://www.dummy.com/dummy-relationship-self-link", relationshipObject.Links.Self); + Assert.Equal("http://www.dummy.com/dummy-relationship-related-link", relationshipObject.Links.Related); + Assert.True(relationshipObject.Data.IsAssigned); + Assert.Equal("20", relationshipObject.Data.ManyValue.Single().Id); } } } diff --git a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs index 661781587c..2058c79e85 100644 --- a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs +++ b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs @@ -1,13 +1,13 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text.Json; using System.Text.RegularExpressions; using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; using UnitTests.TestModels; using Xunit; @@ -368,14 +368,14 @@ public void SerializeSingle_ResourceWithMeta_IncludesMetaInResult() // Assert const string expectedFormatted = @"{ - ""meta"":{ ""test"": ""meta"" }, ""data"":{ ""type"":""oneToManyPrincipals"", ""id"":""10"", ""attributes"":{ ""attributeMember"":null } - } + }, + ""meta"":{ ""test"": ""meta"" } }"; string expected = Regex.Replace(expectedFormatted, @"\s+", ""); @@ -399,7 +399,6 @@ public void SerializeSingle_NullWithLinksAndMeta_StillShowsLinksAndMeta() // Assert const string expectedFormatted = @"{ - ""meta"":{ ""test"": ""meta"" }, ""links"":{ ""self"":""http://www.dummy.com/dummy-self-link"", ""first"":""http://www.dummy.com/dummy-first-link"", @@ -407,7 +406,8 @@ public void SerializeSingle_NullWithLinksAndMeta_StillShowsLinksAndMeta() ""prev"":""http://www.dummy.com/dummy-prev-link"", ""next"":""http://www.dummy.com/dummy-next-link"" }, - ""data"": null + ""data"": null, + ""meta"":{ ""test"": ""meta"" } }"; string expected = Regex.Replace(expectedFormatted, @"\s+", ""); @@ -418,15 +418,18 @@ public void SerializeSingle_NullWithLinksAndMeta_StillShowsLinksAndMeta() public void SerializeError_Error_CanSerialize() { // Arrange - var error = new Error(HttpStatusCode.InsufficientStorage) + var error = new ErrorObject(HttpStatusCode.InsufficientStorage) { Title = "title", Detail = "detail" }; - var errorDocument = new ErrorDocument(error); + var errorDocument = new Document + { + Errors = error.AsList() + }; - string expectedJson = JsonConvert.SerializeObject(new + string expectedJson = JsonSerializer.Serialize(new { errors = new[] { From 5c2c40beee7c2d26111a315f28ada939f916cf07 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 17 Sep 2021 17:22:08 +0200 Subject: [PATCH 4/7] Update ROADMAP.md --- ROADMAP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index ad6d1f0418..1c15a33b2b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -19,8 +19,8 @@ The need for breaking changes has blocked several efforts in the v4.x release, s - [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) -- [ ] Support System.Text.Json [#664](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/664) [#999](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/999) [#233](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/233) -- [ ] 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) +- [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) +- [ ] 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) - [ ] Nullable reference types [#1029](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1029) 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. From 8d94ec91a46cc466c4a1c6aaa923f4e231b957b1 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Sat, 18 Sep 2021 08:49:34 +0200 Subject: [PATCH 5/7] Post-merge fixes --- .../JsonApiDotNetCore.OpenApi.csproj | 1 - .../JsonApiOperationIdSelector.cs | 14 ++-- .../JsonApiSchemaIdSelector.cs | 10 +-- .../OpenApiEndpointConvention.cs | 10 +-- .../ServiceCollectionExtensions.cs | 32 +++---- .../JsonApiDataContractResolver.cs | 15 ++-- .../JsonApiSchemaGenerator.cs | 6 +- .../ResourceObjectSchemaGenerator.cs | 23 +++-- .../SwaggerComponents/ResourceTypeInfo.cs | 6 +- .../ResourceTypeSchemaGenerator.cs | 10 +-- .../JsonKebabCaseNamingPolicy.cs | 83 +++++++++++++++++++ .../LegacyOpenApiIntegrationStartup.cs | 8 +- test/OpenApiTests/OpenApiTests.csproj | 1 - 13 files changed, 147 insertions(+), 72 deletions(-) create mode 100644 test/OpenApiTests/LegacyOpenApiIntegration/JsonKebabCaseNamingPolicy.cs diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj b/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj index 251e13bd19..ab4a38c537 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj @@ -28,6 +28,5 @@ - diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs index 36d6dc6d06..8e45c93397 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using Humanizer; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; @@ -9,7 +10,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Controllers; -using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.OpenApi { @@ -35,17 +35,17 @@ internal sealed class JsonApiOperationIdSelector }; private readonly IControllerResourceMapping _controllerResourceMapping; - private readonly NamingStrategy _namingStrategy; + private readonly JsonNamingPolicy _namingPolicy; private readonly ResourceNameFormatter _formatter; - public JsonApiOperationIdSelector(IControllerResourceMapping controllerResourceMapping, NamingStrategy namingStrategy) + public JsonApiOperationIdSelector(IControllerResourceMapping controllerResourceMapping, JsonNamingPolicy namingPolicy) { ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); - ArgumentGuard.NotNull(namingStrategy, nameof(namingStrategy)); + ArgumentGuard.NotNull(namingPolicy, nameof(namingPolicy)); _controllerResourceMapping = controllerResourceMapping; - _namingStrategy = namingStrategy; - _formatter = new ResourceNameFormatter(namingStrategy); + _namingPolicy = namingPolicy; + _formatter = new ResourceNameFormatter(namingPolicy); } public string GetOperationId(ApiDescription endpoint) @@ -109,7 +109,7 @@ private string ApplyTemplate(string operationIdTemplate, Type primaryResourceTyp // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - return _namingStrategy.GetPropertyName(pascalCaseId, false); + return _namingPolicy.ConvertName(pascalCaseId); } } } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs index a9dac443b0..71d89f7fa5 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs @@ -31,22 +31,22 @@ internal sealed class JsonApiSchemaIdSelector }; private readonly ResourceNameFormatter _formatter; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public JsonApiSchemaIdSelector(ResourceNameFormatter formatter, IResourceContextProvider resourceContextProvider) + public JsonApiSchemaIdSelector(ResourceNameFormatter formatter, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(formatter, nameof(formatter)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _formatter = formatter; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } public string GetSchemaId(Type type) { ArgumentGuard.NotNull(type, nameof(type)); - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(type); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(type); if (resourceContext != null) { diff --git a/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs b/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs index 89829d5d25..5f0f36d56a 100644 --- a/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs +++ b/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs @@ -16,16 +16,16 @@ namespace JsonApiDotNetCore.OpenApi /// internal sealed class OpenApiEndpointConvention : IActionModelConvention { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IControllerResourceMapping _controllerResourceMapping; private readonly EndpointResolver _endpointResolver = new(); - public OpenApiEndpointConvention(IResourceContextProvider resourceContextProvider, IControllerResourceMapping controllerResourceMapping) + public OpenApiEndpointConvention(IResourceGraph resourceGraph, IControllerResourceMapping controllerResourceMapping) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _controllerResourceMapping = controllerResourceMapping; } @@ -71,7 +71,7 @@ private IReadOnlyCollection GetRelationshipsOfPrimaryReso { Type primaryResourceOfEndpointType = _controllerResourceMapping.GetResourceTypeForController(controllerType); - ResourceContext primaryResourceContext = _resourceContextProvider.GetResourceContext(primaryResourceOfEndpointType); + ResourceContext primaryResourceContext = _resourceGraph.GetResourceContext(primaryResourceOfEndpointType); return primaryResourceContext.Relationships; } diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs index f86288113a..aea1faa889 100644 --- a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Text.Json; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.OpenApi.SwaggerComponents; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json.Serialization; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; @@ -57,16 +57,16 @@ private static void AddCustomApiExplorer(IServiceCollection services, IMvcCoreBu private static void AddSwaggerGenerator(IServiceScope scope, IServiceCollection services, Action setupSwaggerGenAction) { var controllerResourceMapping = scope.ServiceProvider.GetRequiredService(); - var resourceContextProvider = scope.ServiceProvider.GetRequiredService(); + var resourceGraph = scope.ServiceProvider.GetRequiredService(); var jsonApiOptions = scope.ServiceProvider.GetRequiredService(); - NamingStrategy namingStrategy = ((DefaultContractResolver)jsonApiOptions.SerializerSettings.ContractResolver)!.NamingStrategy; + var namingPolicy = jsonApiOptions.SerializerOptions.PropertyNamingPolicy; AddSchemaGenerator(services); services.AddSwaggerGen(swaggerGenOptions => { - SetOperationInfo(swaggerGenOptions, controllerResourceMapping, resourceContextProvider, namingStrategy); - SetSchemaIdSelector(swaggerGenOptions, resourceContextProvider, namingStrategy); + SetOperationInfo(swaggerGenOptions, controllerResourceMapping, resourceGraph, namingPolicy); + SetSchemaIdSelector(swaggerGenOptions, resourceGraph, namingPolicy); swaggerGenOptions.DocumentFilter(); setupSwaggerGenAction?.Invoke(swaggerGenOptions); @@ -80,20 +80,20 @@ private static void AddSchemaGenerator(IServiceCollection services) } private static void SetOperationInfo(SwaggerGenOptions swaggerGenOptions, IControllerResourceMapping controllerResourceMapping, - IResourceContextProvider resourceContextProvider, NamingStrategy namingStrategy) + IResourceGraph resourceGraph, JsonNamingPolicy namingPolicy) { - swaggerGenOptions.TagActionsBy(description => GetOperationTags(description, controllerResourceMapping, resourceContextProvider)); + swaggerGenOptions.TagActionsBy(description => GetOperationTags(description, controllerResourceMapping, resourceGraph)); - JsonApiOperationIdSelector jsonApiOperationIdSelector = new(controllerResourceMapping, namingStrategy); + JsonApiOperationIdSelector jsonApiOperationIdSelector = new(controllerResourceMapping, namingPolicy); swaggerGenOptions.CustomOperationIds(jsonApiOperationIdSelector.GetOperationId); } private static IList GetOperationTags(ApiDescription description, IControllerResourceMapping controllerResourceMapping, - IResourceContextProvider resourceContextProvider) + IResourceGraph resourceGraph) { MethodInfo actionMethod = description.ActionDescriptor.GetActionMethod(); Type resourceType = controllerResourceMapping.GetResourceTypeForController(actionMethod.ReflectedType); - ResourceContext resourceContext = resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = resourceGraph.GetResourceContext(resourceType); return new[] { @@ -101,11 +101,11 @@ private static IList GetOperationTags(ApiDescription description, IContr }; } - private static void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions, IResourceContextProvider resourceContextProvider, - NamingStrategy namingStrategy) + private static void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions, IResourceGraph resourceGraph, + JsonNamingPolicy namingPolicy) { - ResourceNameFormatter resourceNameFormatter = new(namingStrategy); - JsonApiSchemaIdSelector jsonApiObjectSchemaSelector = new(resourceNameFormatter, resourceContextProvider); + ResourceNameFormatter resourceNameFormatter = new(namingPolicy); + JsonApiSchemaIdSelector jsonApiObjectSchemaSelector = new(resourceNameFormatter, resourceGraph); swaggerGenOptions.CustomSchemaIds(type => jsonApiObjectSchemaSelector.GetSchemaId(type)); } @@ -127,10 +127,10 @@ private static void AddSwashbuckleCliCompatibility(IServiceScope scope, IMvcCore private static void AddOpenApiEndpointConvention(IServiceScope scope, IMvcCoreBuilder mvcBuilder) { - var resourceContextProvider = scope.ServiceProvider.GetRequiredService(); + var resourceGraph = scope.ServiceProvider.GetRequiredService(); var controllerResourceMapping = scope.ServiceProvider.GetRequiredService(); - mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new OpenApiEndpointConvention(resourceContextProvider, controllerResourceMapping))); + mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new OpenApiEndpointConvention(resourceGraph, controllerResourceMapping))); } } } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs index 199c33d2ff..8df3153cb5 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs @@ -5,7 +5,6 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using Newtonsoft.Json; using Swashbuckle.AspNetCore.Newtonsoft; using Swashbuckle.AspNetCore.SwaggerGen; @@ -17,17 +16,17 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents internal sealed class JsonApiDataContractResolver : ISerializerDataContractResolver { private readonly NewtonsoftDataContractResolver _dataContractResolver; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public JsonApiDataContractResolver(IResourceContextProvider resourceContextProvider, IJsonApiOptions jsonApiOptions) + public JsonApiDataContractResolver(IResourceGraph resourceGraph, IJsonApiOptions jsonApiOptions) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(jsonApiOptions, nameof(jsonApiOptions)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; - JsonSerializerSettings serializerSettings = jsonApiOptions.SerializerSettings ?? new JsonSerializerSettings(); - _dataContractResolver = new NewtonsoftDataContractResolver(serializerSettings); + var serializerOptions = jsonApiOptions.SerializerOptions; + _dataContractResolver = new NewtonsoftDataContractResolver(serializerOptions); } public DataContract GetDataContractForType(Type type) @@ -65,7 +64,7 @@ private static DataContract ReplacePropertiesInDataContract(DataContract dataCon private IList GetDataPropertiesThatExistInResourceContext(Type resourceType, DataContract dataContract) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); var dataProperties = new List(); foreach (DataProperty property in dataContract.ObjectProperties) diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs index b1589e2004..4a75378f1a 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -40,10 +40,10 @@ internal sealed class JsonApiSchemaGenerator : ISchemaGenerator private readonly JsonApiObjectNullabilityProcessor _jsonApiObjectNullabilityProcessor; private readonly SchemaRepositoryAccessor _schemaRepositoryAccessor = new(); - public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceContextProvider resourceContextProvider, IJsonApiOptions jsonApiOptions) + public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions jsonApiOptions) { ArgumentGuard.NotNull(defaultSchemaGenerator, nameof(defaultSchemaGenerator)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(jsonApiOptions, nameof(jsonApiOptions)); _defaultSchemaGenerator = defaultSchemaGenerator; @@ -51,7 +51,7 @@ public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceC _jsonApiObjectNullabilityProcessor = new JsonApiObjectNullabilityProcessor(_schemaRepositoryAccessor); _resourceObjectSchemaGenerator = - new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceContextProvider, jsonApiOptions, _schemaRepositoryAccessor); + new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceGraph, jsonApiOptions, _schemaRepositoryAccessor); } public OpenApiSchema GenerateSchema(Type type, SchemaRepository schemaRepository, MemberInfo memberInfo = null, ParameterInfo parameterInfo = null) diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs index e4c3791e20..f4aff0deb7 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs @@ -3,7 +3,6 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using Microsoft.OpenApi.Models; -using Newtonsoft.Json.Serialization; using Swashbuckle.AspNetCore.SwaggerGen; namespace JsonApiDotNetCore.OpenApi.SwaggerComponents @@ -11,38 +10,38 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents internal sealed class ResourceObjectSchemaGenerator { private readonly SchemaGenerator _defaultSchemaGenerator; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator; private readonly bool _allowClientGeneratedIds; private readonly Func _createFieldObjectBuilderFactory; - public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceContextProvider resourceContextProvider, + public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions jsonApiOptions, ISchemaRepositoryAccessor schemaRepositoryAccessor) { ArgumentGuard.NotNull(defaultSchemaGenerator, nameof(defaultSchemaGenerator)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(jsonApiOptions, nameof(jsonApiOptions)); ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor)); _defaultSchemaGenerator = defaultSchemaGenerator; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _schemaRepositoryAccessor = schemaRepositoryAccessor; - _resourceTypeSchemaGenerator = new ResourceTypeSchemaGenerator(schemaRepositoryAccessor, resourceContextProvider); + _resourceTypeSchemaGenerator = new ResourceTypeSchemaGenerator(schemaRepositoryAccessor, resourceGraph); _allowClientGeneratedIds = jsonApiOptions.AllowClientGeneratedIds; - _createFieldObjectBuilderFactory = CreateFieldObjectBuilderFactory(defaultSchemaGenerator, resourceContextProvider, jsonApiOptions, + _createFieldObjectBuilderFactory = CreateFieldObjectBuilderFactory(defaultSchemaGenerator, resourceGraph, jsonApiOptions, schemaRepositoryAccessor, _resourceTypeSchemaGenerator); } private static Func CreateFieldObjectBuilderFactory(SchemaGenerator defaultSchemaGenerator, - IResourceContextProvider resourceContextProvider, IJsonApiOptions jsonApiOptions, ISchemaRepositoryAccessor schemaRepositoryAccessor, + IResourceGraph resourceGraph, IJsonApiOptions jsonApiOptions, ISchemaRepositoryAccessor schemaRepositoryAccessor, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator) { - NamingStrategy namingStrategy = ((DefaultContractResolver)jsonApiOptions.SerializerSettings.ContractResolver)!.NamingStrategy; - ResourceNameFormatter resourceNameFormatter = new(namingStrategy); - var jsonApiSchemaIdSelector = new JsonApiSchemaIdSelector(resourceNameFormatter, resourceContextProvider); + var namingPolicy = jsonApiOptions.SerializerOptions.PropertyNamingPolicy; + ResourceNameFormatter resourceNameFormatter = new(namingPolicy); + var jsonApiSchemaIdSelector = new JsonApiSchemaIdSelector(resourceNameFormatter, resourceGraph); return resourceTypeInfo => new ResourceFieldObjectSchemaBuilder(resourceTypeInfo, schemaRepositoryAccessor, defaultSchemaGenerator, jsonApiSchemaIdSelector, resourceTypeSchemaGenerator); @@ -54,7 +53,7 @@ public OpenApiSchema GenerateSchema(Type resourceObjectType) (OpenApiSchema fullSchemaForResourceObject, OpenApiSchema referenceSchemaForResourceObject) = EnsureSchemasExist(resourceObjectType); - var resourceTypeInfo = ResourceTypeInfo.Create(resourceObjectType, _resourceContextProvider); + var resourceTypeInfo = ResourceTypeInfo.Create(resourceObjectType, _resourceGraph); ResourceFieldObjectSchemaBuilder fieldObjectBuilder = _createFieldObjectBuilderFactory(resourceTypeInfo); RemoveResourceIdIfPostResourceObject(resourceTypeInfo.ResourceObjectOpenType, fullSchemaForResourceObject); diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeInfo.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeInfo.cs index 5dc13b24b8..67d98dbb85 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeInfo.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeInfo.cs @@ -22,14 +22,14 @@ private ResourceTypeInfo(Type resourceObjectType, Type resourceObjectOpenType, T ResourceType = resourceType; } - public static ResourceTypeInfo Create(Type resourceObjectType, IResourceContextProvider resourceContextProvider) + public static ResourceTypeInfo Create(Type resourceObjectType, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(resourceObjectType, nameof(resourceObjectType)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); Type resourceObjectOpenType = resourceObjectType.GetGenericTypeDefinition(); Type resourceType = resourceObjectType.GenericTypeArguments[0]; - ResourceContext resourceContext = resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = resourceGraph.GetResourceContext(resourceType); return new ResourceTypeInfo(resourceObjectType, resourceObjectOpenType, resourceType, resourceContext); } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs index 2ceba85e18..2c1b43916c 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs @@ -9,16 +9,16 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents internal sealed class ResourceTypeSchemaGenerator { private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly Dictionary _resourceTypeSchemaCache = new(); - public ResourceTypeSchemaGenerator(ISchemaRepositoryAccessor schemaRepositoryAccessor, IResourceContextProvider resourceContextProvider) + public ResourceTypeSchemaGenerator(ISchemaRepositoryAccessor schemaRepositoryAccessor, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _schemaRepositoryAccessor = schemaRepositoryAccessor; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } public OpenApiSchema Get(Type resourceType) @@ -30,7 +30,7 @@ public OpenApiSchema Get(Type resourceType) return referenceSchema; } - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); var fullSchema = new OpenApiSchema { diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/JsonKebabCaseNamingPolicy.cs b/test/OpenApiTests/LegacyOpenApiIntegration/JsonKebabCaseNamingPolicy.cs new file mode 100644 index 0000000000..bd98c17e22 --- /dev/null +++ b/test/OpenApiTests/LegacyOpenApiIntegration/JsonKebabCaseNamingPolicy.cs @@ -0,0 +1,83 @@ +using System; +using System.Text; +using System.Text.Json; + +namespace OpenApiTests.LegacyOpenApiIntegration +{ + // Based on https://github.com/J0rgeSerran0/JsonNamingPolicy + internal sealed class JsonKebabCaseNamingPolicy : JsonNamingPolicy + { + private const char Separator = '-'; + + public static readonly JsonKebabCaseNamingPolicy Instance = new(); + + public override string ConvertName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return string.Empty; + } + + ReadOnlySpan spanName = name.Trim(); + + var stringBuilder = new StringBuilder(); + bool addCharacter = true; + + bool isNextLower = false; + bool isNextUpper = false; + bool isNextSpace = false; + + for (int position = 0; position < spanName.Length; position++) + { + if (position != 0) + { + bool isCurrentSpace = spanName[position] == 32; + bool isPreviousSpace = spanName[position - 1] == 32; + bool isPreviousSeparator = spanName[position - 1] == 95; + + if (position + 1 != spanName.Length) + { + isNextLower = spanName[position + 1] > 96 && spanName[position + 1] < 123; + isNextUpper = spanName[position + 1] > 64 && spanName[position + 1] < 91; + isNextSpace = spanName[position + 1] == 32; + } + + if (isCurrentSpace && (isPreviousSpace || isPreviousSeparator || isNextUpper || isNextSpace)) + { + addCharacter = false; + } + else + { + bool isCurrentUpper = spanName[position] > 64 && spanName[position] < 91; + bool isPreviousLower = spanName[position - 1] > 96 && spanName[position - 1] < 123; + bool isPreviousNumber = spanName[position - 1] > 47 && spanName[position - 1] < 58; + + if (isCurrentUpper && (isPreviousLower || isPreviousNumber || isNextLower || isNextSpace)) + { + stringBuilder.Append(Separator); + } + else + { + if (isCurrentSpace) + { + stringBuilder.Append(Separator); + addCharacter = false; + } + } + } + } + + if (addCharacter) + { + stringBuilder.Append(spanName[position]); + } + else + { + addCharacter = true; + } + } + + return stringBuilder.ToString().ToLower(); + } + } +} diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs index 02dbf96e25..2f72e6e97e 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs @@ -2,7 +2,6 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json.Serialization; namespace OpenApiTests.LegacyOpenApiIntegration { @@ -16,11 +15,8 @@ protected override void SetJsonApiOptions(JsonApiOptions options) options.Namespace = "api/v1"; options.DefaultAttrCapabilities = AttrCapabilities.AllowView; - - options.SerializerSettings.ContractResolver = new DefaultContractResolver - { - NamingStrategy = new KebabCaseNamingStrategy() - }; + options.SerializerOptions.PropertyNamingPolicy = JsonKebabCaseNamingPolicy.Instance; + options.SerializerOptions.DictionaryKeyPolicy = JsonKebabCaseNamingPolicy.Instance; } } } diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index 04355533a7..712134cb82 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -4,7 +4,6 @@ - From 41c24c49ff835deb6ebd7bb0d07fe253ec2b13b0 Mon Sep 17 00:00:00 2001 From: maurei Date: Sat, 18 Sep 2021 13:40:27 +0200 Subject: [PATCH 6/7] Replace NewtonsoftDataContractResolver with JsonSerializerDataContractResolver --- .../JsonApiSchemaIdSelector.cs | 2 +- .../ServiceCollectionExtensions.cs | 5 +- .../JsonApiDataContractResolver.cs | 10 +- .../ResourceObjectSchemaGenerator.cs | 11 +- .../LegacyClient/ResponseTests.cs | 2 +- .../LegacyOpenApiIntegrationStartup.cs | 2 + .../LegacyOpenApiIntegration/swagger.json | 192 +++++++++--------- 7 files changed, 113 insertions(+), 111 deletions(-) diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs index 71d89f7fa5..e87497c938 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs @@ -46,7 +46,7 @@ public string GetSchemaId(Type type) { ArgumentGuard.NotNull(type, nameof(type)); - ResourceContext resourceContext = _resourceGraph.GetResourceContext(type); + ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(type); if (resourceContext != null) { diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs index aea1faa889..855bfee84e 100644 --- a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -59,7 +59,7 @@ private static void AddSwaggerGenerator(IServiceScope scope, IServiceCollection var controllerResourceMapping = scope.ServiceProvider.GetRequiredService(); var resourceGraph = scope.ServiceProvider.GetRequiredService(); var jsonApiOptions = scope.ServiceProvider.GetRequiredService(); - var namingPolicy = jsonApiOptions.SerializerOptions.PropertyNamingPolicy; + JsonNamingPolicy namingPolicy = jsonApiOptions.SerializerOptions.PropertyNamingPolicy; AddSchemaGenerator(services); @@ -101,8 +101,7 @@ private static IList GetOperationTags(ApiDescription description, IContr }; } - private static void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions, IResourceGraph resourceGraph, - JsonNamingPolicy namingPolicy) + private static void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions, IResourceGraph resourceGraph, JsonNamingPolicy namingPolicy) { ResourceNameFormatter resourceNameFormatter = new(namingPolicy); JsonApiSchemaIdSelector jsonApiObjectSchemaSelector = new(resourceNameFormatter, resourceGraph); diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs index 8df3153cb5..a84acc86aa 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs @@ -2,20 +2,20 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.Json; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using Swashbuckle.AspNetCore.Newtonsoft; using Swashbuckle.AspNetCore.SwaggerGen; namespace JsonApiDotNetCore.OpenApi.SwaggerComponents { /// - /// For schema generation, we rely on from Swashbuckle for all but our own JSON:API types. + /// For schema generation, we rely on from Swashbuckle for all but our own JSON:API types. /// internal sealed class JsonApiDataContractResolver : ISerializerDataContractResolver { - private readonly NewtonsoftDataContractResolver _dataContractResolver; + private readonly JsonSerializerDataContractResolver _dataContractResolver; private readonly IResourceGraph _resourceGraph; public JsonApiDataContractResolver(IResourceGraph resourceGraph, IJsonApiOptions jsonApiOptions) @@ -25,8 +25,8 @@ public JsonApiDataContractResolver(IResourceGraph resourceGraph, IJsonApiOptions _resourceGraph = resourceGraph; - var serializerOptions = jsonApiOptions.SerializerOptions; - _dataContractResolver = new NewtonsoftDataContractResolver(serializerOptions); + JsonSerializerOptions serializerOptions = jsonApiOptions.SerializerOptions; + _dataContractResolver = new JsonSerializerDataContractResolver(serializerOptions); } public DataContract GetDataContractForType(Type type) diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs index f4aff0deb7..5aabb571a7 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using Microsoft.OpenApi.Models; @@ -16,8 +17,8 @@ internal sealed class ResourceObjectSchemaGenerator private readonly bool _allowClientGeneratedIds; private readonly Func _createFieldObjectBuilderFactory; - public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, - IJsonApiOptions jsonApiOptions, ISchemaRepositoryAccessor schemaRepositoryAccessor) + public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions jsonApiOptions, + ISchemaRepositoryAccessor schemaRepositoryAccessor) { ArgumentGuard.NotNull(defaultSchemaGenerator, nameof(defaultSchemaGenerator)); ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); @@ -31,15 +32,15 @@ public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IRe _resourceTypeSchemaGenerator = new ResourceTypeSchemaGenerator(schemaRepositoryAccessor, resourceGraph); _allowClientGeneratedIds = jsonApiOptions.AllowClientGeneratedIds; - _createFieldObjectBuilderFactory = CreateFieldObjectBuilderFactory(defaultSchemaGenerator, resourceGraph, jsonApiOptions, - schemaRepositoryAccessor, _resourceTypeSchemaGenerator); + _createFieldObjectBuilderFactory = CreateFieldObjectBuilderFactory(defaultSchemaGenerator, resourceGraph, jsonApiOptions, schemaRepositoryAccessor, + _resourceTypeSchemaGenerator); } private static Func CreateFieldObjectBuilderFactory(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions jsonApiOptions, ISchemaRepositoryAccessor schemaRepositoryAccessor, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator) { - var namingPolicy = jsonApiOptions.SerializerOptions.PropertyNamingPolicy; + JsonNamingPolicy namingPolicy = jsonApiOptions.SerializerOptions.PropertyNamingPolicy; ResourceNameFormatter resourceNameFormatter = new(namingPolicy); var jsonApiSchemaIdSelector = new JsonApiSchemaIdSelector(resourceNameFormatter, resourceGraph); diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index dfa5891dca..14e3dfa130 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -187,7 +187,7 @@ public async Task Getting_resource_translates_response() document.Data.Attributes.ServicesOnBoard.Should().BeNull(); document.Data.Attributes.FinalDestination.Should().BeNull(); document.Data.Attributes.StopOverDestination.Should().BeNull(); - document.Data.Attributes.OperatedBy.Should().Be(default(Airline)); + document.Data.Attributes.OperatedBy.Should().Be(default); } [Fact] diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs index 2f72e6e97e..916a40fa4d 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; @@ -17,6 +18,7 @@ protected override void SetJsonApiOptions(JsonApiOptions options) options.DefaultAttrCapabilities = AttrCapabilities.AllowView; options.SerializerOptions.PropertyNamingPolicy = JsonKebabCaseNamingPolicy.Instance; options.SerializerOptions.DictionaryKeyPolicy = JsonKebabCaseNamingPolicy.Instance; + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); } } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json index 3ed426bf24..d8c9bbda06 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json @@ -1870,6 +1870,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/airplane-data-in-response" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -1879,12 +1885,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/airplane-data-in-response" - } } }, "additionalProperties": false @@ -1990,6 +1990,9 @@ ], "type": "object", "properties": { + "data": { + "$ref": "#/components/schemas/airplane-data-in-response" + }, "meta": { "type": "object", "additionalProperties": {} @@ -1999,9 +2002,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-document" - }, - "data": { - "$ref": "#/components/schemas/airplane-data-in-response" } }, "additionalProperties": false @@ -2119,6 +2119,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-attendant-data-in-response" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -2128,12 +2134,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/flight-attendant-data-in-response" - } } }, "additionalProperties": false @@ -2231,6 +2231,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -2240,12 +2246,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/flight-attendant-identifier" - } } }, "additionalProperties": false @@ -2257,16 +2257,6 @@ ], "type": "object", "properties": { - "meta": { - "type": "object", - "additionalProperties": {} - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" - }, - "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-document" - }, "data": { "oneOf": [ { @@ -2276,6 +2266,16 @@ "$ref": "#/components/schemas/null-value" } ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-identifier-document" } }, "additionalProperties": false @@ -2311,6 +2311,9 @@ ], "type": "object", "properties": { + "data": { + "$ref": "#/components/schemas/flight-attendant-data-in-response" + }, "meta": { "type": "object", "additionalProperties": {} @@ -2320,9 +2323,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-document" - }, - "data": { - "$ref": "#/components/schemas/flight-attendant-data-in-response" } }, "additionalProperties": false @@ -2370,16 +2370,6 @@ ], "type": "object", "properties": { - "meta": { - "type": "object", - "additionalProperties": {} - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" - }, - "links": { - "$ref": "#/components/schemas/links-in-resource-document" - }, "data": { "oneOf": [ { @@ -2389,6 +2379,16 @@ "$ref": "#/components/schemas/null-value" } ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" } }, "additionalProperties": false @@ -2459,6 +2459,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-data-in-response" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -2468,12 +2474,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/flight-data-in-response" - } } }, "additionalProperties": false @@ -2568,6 +2568,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-identifier" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -2577,12 +2583,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/flight-identifier" - } } }, "additionalProperties": false @@ -2618,6 +2618,9 @@ ], "type": "object", "properties": { + "data": { + "$ref": "#/components/schemas/flight-data-in-response" + }, "meta": { "type": "object", "additionalProperties": {} @@ -2627,9 +2630,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-document" - }, - "data": { - "$ref": "#/components/schemas/flight-data-in-response" } }, "additionalProperties": false @@ -2875,6 +2875,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/passenger-data-in-response" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -2884,12 +2890,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/passenger-data-in-response" - } } }, "additionalProperties": false @@ -2944,6 +2944,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/passenger-identifier" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -2953,12 +2959,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/passenger-identifier" - } } }, "additionalProperties": false @@ -2990,18 +2990,18 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + }, "links": { "$ref": "#/components/schemas/links-in-relationship-object" }, "meta": { "type": "object", "additionalProperties": {} - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/flight-attendant-identifier" - } } }, "additionalProperties": false @@ -3027,18 +3027,18 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-identifier" + } + }, "links": { "$ref": "#/components/schemas/links-in-relationship-object" }, "meta": { "type": "object", "additionalProperties": {} - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/flight-identifier" - } } }, "additionalProperties": false @@ -3064,18 +3064,18 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/passenger-identifier" + } + }, "links": { "$ref": "#/components/schemas/links-in-relationship-object" }, "meta": { "type": "object", "additionalProperties": {} - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/passenger-identifier" - } } }, "additionalProperties": false @@ -3105,13 +3105,6 @@ ], "type": "object", "properties": { - "links": { - "$ref": "#/components/schemas/links-in-relationship-object" - }, - "meta": { - "type": "object", - "additionalProperties": {} - }, "data": { "oneOf": [ { @@ -3121,6 +3114,13 @@ "$ref": "#/components/schemas/null-value" } ] + }, + "links": { + "$ref": "#/components/schemas/links-in-relationship-object" + }, + "meta": { + "type": "object", + "additionalProperties": {} } }, "additionalProperties": false From 4f5ee425b2863e69e476b91f39101ca4717f7c4b Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 20 Sep 2021 10:55:59 +0200 Subject: [PATCH 7/7] Small cleanups --- .../SwaggerComponents/JsonApiDataContractResolver.cs | 9 +++------ .../SwaggerComponents/JsonApiSchemaGenerator.cs | 8 +++----- .../ResourceObjectSchemaGenerator.cs | 12 ++++++------ 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs index a84acc86aa..c5723fa0ec 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text.Json; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -18,15 +17,13 @@ internal sealed class JsonApiDataContractResolver : ISerializerDataContractResol private readonly JsonSerializerDataContractResolver _dataContractResolver; private readonly IResourceGraph _resourceGraph; - public JsonApiDataContractResolver(IResourceGraph resourceGraph, IJsonApiOptions jsonApiOptions) + public JsonApiDataContractResolver(IResourceGraph resourceGraph, IJsonApiOptions options) { ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - ArgumentGuard.NotNull(jsonApiOptions, nameof(jsonApiOptions)); + ArgumentGuard.NotNull(options, nameof(options)); _resourceGraph = resourceGraph; - - JsonSerializerOptions serializerOptions = jsonApiOptions.SerializerOptions; - _dataContractResolver = new JsonSerializerDataContractResolver(serializerOptions); + _dataContractResolver = new JsonSerializerDataContractResolver(options.SerializerOptions); } public DataContract GetDataContractForType(Type type) diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs index 4a75378f1a..05e17fabce 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -40,18 +40,16 @@ internal sealed class JsonApiSchemaGenerator : ISchemaGenerator private readonly JsonApiObjectNullabilityProcessor _jsonApiObjectNullabilityProcessor; private readonly SchemaRepositoryAccessor _schemaRepositoryAccessor = new(); - public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions jsonApiOptions) + public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options) { ArgumentGuard.NotNull(defaultSchemaGenerator, nameof(defaultSchemaGenerator)); ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - ArgumentGuard.NotNull(jsonApiOptions, nameof(jsonApiOptions)); + ArgumentGuard.NotNull(options, nameof(options)); _defaultSchemaGenerator = defaultSchemaGenerator; _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(_schemaRepositoryAccessor); _jsonApiObjectNullabilityProcessor = new JsonApiObjectNullabilityProcessor(_schemaRepositoryAccessor); - - _resourceObjectSchemaGenerator = - new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceGraph, jsonApiOptions, _schemaRepositoryAccessor); + _resourceObjectSchemaGenerator = new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceGraph, options, _schemaRepositoryAccessor); } public OpenApiSchema GenerateSchema(Type type, SchemaRepository schemaRepository, MemberInfo memberInfo = null, ParameterInfo parameterInfo = null) diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs index 5aabb571a7..baf380972d 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs @@ -17,12 +17,12 @@ internal sealed class ResourceObjectSchemaGenerator private readonly bool _allowClientGeneratedIds; private readonly Func _createFieldObjectBuilderFactory; - public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions jsonApiOptions, + public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options, ISchemaRepositoryAccessor schemaRepositoryAccessor) { ArgumentGuard.NotNull(defaultSchemaGenerator, nameof(defaultSchemaGenerator)); ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - ArgumentGuard.NotNull(jsonApiOptions, nameof(jsonApiOptions)); + ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor)); _defaultSchemaGenerator = defaultSchemaGenerator; @@ -30,17 +30,17 @@ public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IRe _schemaRepositoryAccessor = schemaRepositoryAccessor; _resourceTypeSchemaGenerator = new ResourceTypeSchemaGenerator(schemaRepositoryAccessor, resourceGraph); - _allowClientGeneratedIds = jsonApiOptions.AllowClientGeneratedIds; + _allowClientGeneratedIds = options.AllowClientGeneratedIds; - _createFieldObjectBuilderFactory = CreateFieldObjectBuilderFactory(defaultSchemaGenerator, resourceGraph, jsonApiOptions, schemaRepositoryAccessor, + _createFieldObjectBuilderFactory = CreateFieldObjectBuilderFactory(defaultSchemaGenerator, resourceGraph, options, schemaRepositoryAccessor, _resourceTypeSchemaGenerator); } private static Func CreateFieldObjectBuilderFactory(SchemaGenerator defaultSchemaGenerator, - IResourceGraph resourceGraph, IJsonApiOptions jsonApiOptions, ISchemaRepositoryAccessor schemaRepositoryAccessor, + IResourceGraph resourceGraph, IJsonApiOptions options, ISchemaRepositoryAccessor schemaRepositoryAccessor, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator) { - JsonNamingPolicy namingPolicy = jsonApiOptions.SerializerOptions.PropertyNamingPolicy; + JsonNamingPolicy namingPolicy = options.SerializerOptions.PropertyNamingPolicy; ResourceNameFormatter resourceNameFormatter = new(namingPolicy); var jsonApiSchemaIdSelector = new JsonApiSchemaIdSelector(resourceNameFormatter, resourceGraph);