From 8534c41d9c6c32c4b121d80cf6d00264cfa785f8 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 20 Sep 2021 11:13:58 +0200 Subject: [PATCH 01/49] Replaced suppression with fix --- src/JsonApiDotNetCore/Serialization/JsonApiReader.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index af634f9876..94c1e6b249 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -83,9 +83,7 @@ public async Task ReadAsync(InputFormatterContext context) ValidateRequestBody(model, body, context.HttpContext.Request); } - // ReSharper disable once AssignNullToNotNullAttribute - // Justification: According to JSON:API we must return 200 OK without a body in some cases. - return await InputFormatterResult.SuccessAsync(model); + return model == null ? await InputFormatterResult.NoValueAsync() : await InputFormatterResult.SuccessAsync(model); } private async Task GetRequestBodyAsync(Stream bodyStream) From f502628edb3f0e8735f4a797c851dba3a820df66 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 20 Sep 2021 14:32:30 +0200 Subject: [PATCH 02/49] Fixed: do not duplicate the list of JSON:API errors in meta stack trace, but do write them when logging --- .../Errors/InvalidModelStateException.cs | 10 +++++++--- src/JsonApiDotNetCore/Errors/JsonApiException.cs | 7 +++++-- src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs | 9 ++++++--- .../ExceptionHandling/ExceptionHandlerTests.cs | 2 +- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs index 4ca8586b17..046f8de471 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs @@ -116,10 +116,14 @@ private static ErrorObject FromModelError(ModelError modelError, string attribut if (includeExceptionStackTraceInErrors && modelError.Exception != null) { - string[] stackTraceLines = modelError.Exception.Demystify().ToString().Split(Environment.NewLine); + Exception exception = modelError.Exception.Demystify(); + string[] stackTraceLines = exception.ToString().Split(Environment.NewLine); - error.Meta ??= new Dictionary(); - error.Meta["StackTrace"] = stackTraceLines; + if (stackTraceLines.Any()) + { + error.Meta ??= new Dictionary(); + error.Meta["StackTrace"] = stackTraceLines; + } } return error; diff --git a/src/JsonApiDotNetCore/Errors/JsonApiException.cs b/src/JsonApiDotNetCore/Errors/JsonApiException.cs index 13d3b6a745..ae2cbcdca0 100644 --- a/src/JsonApiDotNetCore/Errors/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Errors/JsonApiException.cs @@ -24,8 +24,6 @@ public class JsonApiException : Exception public IReadOnlyList Errors { get; } - public override string Message => $"Errors = {JsonSerializer.Serialize(Errors, SerializerOptions)}"; - public JsonApiException(ErrorObject error, Exception innerException = null) : base(null, innerException) { @@ -42,5 +40,10 @@ public JsonApiException(IEnumerable errors, Exception innerExceptio Errors = errorList; } + + public string GetSummary() + { + return $"{nameof(JsonApiException)}: Errors = {JsonSerializer.Serialize(Errors, SerializerOptions)}"; + } } } diff --git a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs index 5dc6bf6e5d..6bbb2b1fb1 100644 --- a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs +++ b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs @@ -67,7 +67,7 @@ protected virtual string GetLogMessage(Exception exception) { ArgumentGuard.NotNull(exception, nameof(exception)); - return exception.Message; + return exception is JsonApiException jsonApiException ? jsonApiException.GetSummary() : exception.Message; } protected virtual Document CreateErrorDocument(Exception exception) @@ -103,8 +103,11 @@ private void ApplyOptions(ErrorObject error, Exception exception) { string[] stackTraceLines = resultException.ToString().Split(Environment.NewLine); - error.Meta ??= new Dictionary(); - error.Meta["StackTrace"] = stackTraceLines; + if (stackTraceLines.Any()) + { + error.Meta ??= new Dictionary(); + error.Meta["StackTrace"] = stackTraceLines; + } } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs index ff8a61471f..be12b48795 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs @@ -116,7 +116,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Detail.Should().Be("Exception has been thrown by the target of an invocation."); IEnumerable stackTraceLines = ((JsonElement)error.Meta["stackTrace"]).EnumerateArray().Select(token => token.GetString()); - stackTraceLines.Should().ContainMatch("* System.InvalidOperationException: Article status could not be determined.*"); + stackTraceLines.Should().ContainMatch("*at object System.Reflection.*"); loggerFactory.Logger.Messages.Should().HaveCount(1); loggerFactory.Logger.Messages.Single().LogLevel.Should().Be(LogLevel.Error); From afe119db40e1508fbe858c63a5b06dc8dc64d58d Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 20 Sep 2021 16:46:31 +0200 Subject: [PATCH 03/49] Breaking: Added option to write request body in meta when unable to read it (false by default) --- .../JsonApiDotNetCoreExample/Startup.cs | 1 + src/Examples/MultiDbContextExample/Startup.cs | 1 + .../Configuration/IJsonApiOptions.cs | 7 +++- .../Configuration/JsonApiOptions.cs | 3 ++ .../Configuration/ResourceGraph.cs | 2 +- .../Errors/InvalidRequestBodyException.cs | 19 +++------- .../Middleware/ExceptionHandler.cs | 33 +++++++++++------ .../Serialization/JsonApiReader.cs | 3 +- .../ExceptionHandlerTests.cs | 36 +++++++++++++++++++ .../ReadWrite/Creating/CreateResourceTests.cs | 24 +++++++++---- ...eateResourceWithToManyRelationshipTests.cs | 28 +++++++++++---- ...reateResourceWithToOneRelationshipTests.cs | 22 ++++++++---- .../AddToToManyRelationshipTests.cs | 20 ++++++++--- .../RemoveFromToManyRelationshipTests.cs | 20 ++++++++--- .../ReplaceToManyRelationshipTests.cs | 20 ++++++++--- .../UpdateToOneRelationshipTests.cs | 16 ++++++--- .../ReplaceToManyRelationshipTests.cs | 24 +++++++++---- .../Updating/Resources/UpdateResourceTests.cs | 36 +++++++++++++------ .../Resources/UpdateToOneRelationshipTests.cs | 20 ++++++++--- test/TestBuildingBlocks/TestableStartup.cs | 1 + 20 files changed, 247 insertions(+), 89 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startup.cs index fb75cfaef8..f14c1df8ce 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startup.cs @@ -55,6 +55,7 @@ public void ConfigureServices(IServiceCollection services) options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); #if DEBUG options.IncludeExceptionStackTraceInErrors = true; + options.IncludeRequestBodyInErrors = true; #endif }, discovery => discovery.AddCurrentAssembly()); } diff --git a/src/Examples/MultiDbContextExample/Startup.cs b/src/Examples/MultiDbContextExample/Startup.cs index bc76c3cd9a..705bf8ef4c 100644 --- a/src/Examples/MultiDbContextExample/Startup.cs +++ b/src/Examples/MultiDbContextExample/Startup.cs @@ -25,6 +25,7 @@ public void ConfigureServices(IServiceCollection services) services.AddJsonApi(options => { options.IncludeExceptionStackTraceInErrors = true; + options.IncludeRequestBodyInErrors = true; }, dbContextTypes: new[] { typeof(DbContextA), diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index 4b5d36c421..3318a71679 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -30,10 +30,15 @@ public interface IJsonApiOptions bool IncludeJsonApiVersion { get; } /// - /// Whether or not stack traces should be serialized in . False by default. + /// Whether or not stack traces should be included in . False by default. /// bool IncludeExceptionStackTraceInErrors { get; } + /// + /// Whether or not the request body should be included in when it is invalid. False by default. + /// + bool IncludeRequestBodyInErrors { get; } + /// /// Use relative links for all resources. False by default. /// diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 4806c36248..05472aff5e 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -38,6 +38,9 @@ public sealed class JsonApiOptions : IJsonApiOptions /// public bool IncludeExceptionStackTraceInErrors { get; set; } + /// + public bool IncludeRequestBodyInErrors { get; set; } + /// public bool UseRelativeLinks { get; set; } diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs index f65755b38d..48654ea0a8 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs @@ -54,7 +54,7 @@ public ResourceContext GetResourceContext(string publicName) /// public ResourceContext TryGetResourceContext(string publicName) { - ArgumentGuard.NotNullNorEmpty(publicName, nameof(publicName)); + ArgumentGuard.NotNull(publicName, nameof(publicName)); return _resourceContextsByPublicName.TryGetValue(publicName, out ResourceContext resourceContext) ? resourceContext : null; } diff --git a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs index c435c66b2d..1d53ca3635 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs @@ -12,32 +12,23 @@ namespace JsonApiDotNetCore.Errors [PublicAPI] public sealed class InvalidRequestBodyException : JsonApiException { + public string RequestBody { get; } + public InvalidRequestBodyException(string reason, string details, string requestBody, Exception innerException = null) : base(new ErrorObject(HttpStatusCode.UnprocessableEntity) { Title = reason != null ? $"Failed to deserialize request body: {reason}" : "Failed to deserialize request body.", - Detail = FormatErrorDetail(details, requestBody, innerException) + Detail = FormatErrorDetail(details, innerException) }, innerException) { + RequestBody = requestBody; } - private static string FormatErrorDetail(string details, string requestBody, Exception innerException) + private static string FormatErrorDetail(string details, Exception innerException) { var builder = new StringBuilder(); builder.Append(details ?? innerException?.Message); - if (requestBody != null) - { - if (builder.Length > 0) - { - builder.Append(" - "); - } - - builder.Append("Request body: <<"); - builder.Append(requestBody); - builder.Append(">>"); - } - return builder.Length > 0 ? builder.ToString() : null; } } diff --git a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs index 6bbb2b1fb1..205ca6a208 100644 --- a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs +++ b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs @@ -84,31 +84,42 @@ protected virtual Document CreateErrorDocument(Exception exception) Detail = exception.Message }.AsArray(); - foreach (ErrorObject error in errors) + var document = new Document { - ApplyOptions(error, exception); + Errors = errors.ToList() + }; + + if (_options.IncludeExceptionStackTraceInErrors && exception is not InvalidModelStateException) + { + IncludeStackTraces(exception, document.Errors); } - return new Document + if (_options.IncludeRequestBodyInErrors && exception is InvalidRequestBodyException { RequestBody: { } } invalidRequestBodyException) { - Errors = errors.ToList() - }; + IncludeRequestBody(invalidRequestBodyException, document); + } + + return document; } - private void ApplyOptions(ErrorObject error, Exception exception) + private void IncludeStackTraces(Exception exception, IList errors) { - Exception resultException = exception is InvalidModelStateException ? null : exception; + string[] stackTraceLines = exception.ToString().Split(Environment.NewLine); - if (resultException != null && _options.IncludeExceptionStackTraceInErrors) + if (stackTraceLines.Any()) { - string[] stackTraceLines = resultException.ToString().Split(Environment.NewLine); - - if (stackTraceLines.Any()) + foreach (ErrorObject error in errors) { error.Meta ??= new Dictionary(); error.Meta["StackTrace"] = stackTraceLines; } } } + + private static void IncludeRequestBody(InvalidRequestBodyException exception, Document document) + { + document.Meta ??= new Dictionary(); + document.Meta["RequestBody"] = exception.RequestBody; + } } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index 94c1e6b249..df78c648d0 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -99,8 +99,7 @@ private InvalidRequestBodyException ToInvalidRequestBodyException(JsonApiSeriali return new InvalidRequestBodyException(exception.GenericMessage, exception.SpecificMessage, body, exception); } - // In contrast to resource endpoints, we don't include the request body for operations because they are usually very long. - var requestException = new InvalidRequestBodyException(exception.GenericMessage, exception.SpecificMessage, null, exception.InnerException); + var requestException = new InvalidRequestBodyException(exception.GenericMessage, exception.SpecificMessage, body, exception.InnerException); if (exception.AtomicOperationIndex != null) { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs index be12b48795..50323fbe98 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs @@ -80,11 +80,45 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Detail.Should().Be("Article with code 'X123' is no longer available."); ((JsonElement)error.Meta["support"]).GetString().Should().Be("Please contact us for info about similar articles at company@email.com."); + responseDocument.Meta.Should().BeNull(); + loggerFactory.Logger.Messages.Should().HaveCount(1); loggerFactory.Logger.Messages.Single().LogLevel.Should().Be(LogLevel.Warning); loggerFactory.Logger.Messages.Single().Text.Should().Contain("Article with code 'X123' is no longer available."); } + [Fact] + public async Task Logs_and_produces_error_response_on_deserialization_failure() + { + // Arrange + var loggerFactory = _testContext.Factory.Services.GetRequiredService(); + loggerFactory.Logger.Clear(); + + const string requestBody = @"{ ""data"": { ""type"": """" } }"; + + const string route = "/consumerArticles"; + + // 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: Request body includes unknown resource type."); + error.Detail.Should().Be("Resource type '' does not exist."); + + IEnumerable stackTraceLines = ((JsonElement)error.Meta["stackTrace"]).EnumerateArray().Select(token => token.GetString()); + stackTraceLines.Should().NotBeEmpty(); + + responseDocument.Meta["requestBody"].ToString().Should().Be(requestBody); + + loggerFactory.Logger.Messages.Should().BeEmpty(); + } + [Fact] public async Task Logs_and_produces_error_response_on_serialization_failure() { @@ -118,6 +152,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => IEnumerable stackTraceLines = ((JsonElement)error.Meta["stackTrace"]).EnumerateArray().Select(token => token.GetString()); stackTraceLines.Should().ContainMatch("*at object System.Reflection.*"); + responseDocument.Meta.Should().BeNull(); + loggerFactory.Logger.Messages.Should().HaveCount(1); loggerFactory.Logger.Messages.Single().LogLevel.Should().Be(LogLevel.Error); loggerFactory.Logger.Messages.Single().Text.Should().Contain("Exception has been thrown by the target of an invocation."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 9a148f48b1..84d976a5d9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -423,7 +423,9 @@ public async Task Cannot_create_resource_for_missing_type() 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: <<"); + error.Detail.Should().Be("Expected 'type' element in 'data' element."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -454,7 +456,9 @@ public async Task Cannot_create_resource_for_unknown_type() 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 '{Unknown.ResourceType}' does not exist. - Request body: <<"); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -541,7 +545,9 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() 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().Be("Setting the initial value of 'isImportant' is not allowed."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -573,7 +579,9 @@ public async Task Cannot_create_resource_with_readonly_attribute() 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().Be("Attribute 'isDeprecated' is read-only."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -595,7 +603,9 @@ public async Task Cannot_create_resource_for_broken_JSON_request_body() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().Match("'{' is invalid after a property name. * - Request body: <<*"); + error.Detail.Should().StartWith("'{' is invalid after a property name."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -627,9 +637,9 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() 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 attribute 'dueAt' with value 'not-a-valid-time' of type 'String' to type 'Nullable'."); - error.Detail.Should().StartWith("Failed to convert attribute 'dueAt' with value 'not-a-valid-time' " + - "of type 'String' to type 'Nullable'. - Request body: <<"); + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index e9eae47336..b261199dc3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -358,7 +358,9 @@ public async Task Cannot_create_for_missing_relationship_type() 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: <<"); + error.Detail.Should().Be("Expected 'type' element in 'subscribers' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -400,7 +402,9 @@ public async Task Cannot_create_for_unknown_relationship_type() 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 '{Unknown.ResourceType}' does not exist. - Request body: <<"); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -441,7 +445,9 @@ public async Task Cannot_create_for_missing_relationship_ID() 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: <<"); + error.Detail.Should().Be("Expected 'id' element in 'subscribers' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -538,7 +544,9 @@ public async Task Cannot_create_on_relationship_type_mismatch() 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: <<"); + error.Detail.Should().Be("Relationship 'subscribers' contains incompatible resource type 'rgbColors'."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -639,7 +647,9 @@ public async Task Cannot_create_with_null_data_in_OneToMany_relationship() 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: <<"); + error.Detail.Should().Be("Expected data[] element for 'subscribers' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -674,7 +684,9 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() 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: <<"); + error.Detail.Should().Be("Expected data[] element for 'tags' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -719,7 +731,9 @@ public async Task Cannot_create_resource_with_local_ID() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Local IDs cannot be used at this endpoint. - Request body: <<"); + error.Detail.Should().Be("Local IDs cannot be used at this endpoint."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index acd6f6136e..427d2d065c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -318,7 +318,9 @@ public async Task Cannot_create_for_missing_relationship_type() 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: <<"); + error.Detail.Should().Be("Expected 'type' element in 'assignee' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -357,7 +359,9 @@ public async Task Cannot_create_for_unknown_relationship_type() 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 '{Unknown.ResourceType}' does not exist. - Request body: <<"); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -395,7 +399,9 @@ public async Task Cannot_create_for_missing_relationship_ID() 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: <<"); + error.Detail.Should().Be("Expected 'id' element in 'assignee' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -475,7 +481,9 @@ public async Task Cannot_create_on_relationship_type_mismatch() 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: <<"); + error.Detail.Should().Be("Relationship 'assignee' contains incompatible resource type 'rgbColors'."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -596,7 +604,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected single data element for 'assignee' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -638,7 +648,7 @@ public async Task Cannot_create_resource_with_local_ID() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Local IDs cannot be used at this endpoint. - Request body: <<"); + error.Detail.Should().Be("Local IDs cannot be used at this endpoint."); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index d66a425101..3ea7bad7c6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -242,7 +242,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected 'type' element in 'data' element."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -282,7 +284,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist. - Request body: <<"); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -321,7 +325,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().BeNull(); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -705,7 +711,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected data[] element for 'subscribers' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -738,7 +746,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected data[] element for 'tags' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index ed1f9755ab..177be7be30 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -359,7 +359,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected 'type' element in 'data' element."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -399,7 +401,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist. - Request body: <<"); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -438,7 +442,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().BeNull(); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -824,7 +830,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected data[] element for 'subscribers' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -857,7 +865,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected data[] element for 'tags' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index 3b439c1cec..10c1743f6a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -274,7 +274,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected 'type' element in 'data' element."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -314,7 +316,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist. - Request body: <<"); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -353,7 +357,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().BeNull(); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -697,7 +703,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected data[] element for 'subscribers' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -730,7 +738,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected data[] element for 'tags' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index afe4fc3c97..95c34e3c62 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -304,7 +304,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected 'type' element in 'data' element."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -341,7 +343,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist. - Request body: <<"); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -377,7 +381,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().BeNull(); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -606,7 +612,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected single data element for 'assignee' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index b225589c4e..ffa47a0900 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -442,7 +442,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected 'type' element in 'subscribers' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -493,7 +495,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist. - Request body: <<"); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -543,7 +547,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected 'id' element in 'subscribers' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -687,7 +693,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Relationship 'subscribers' contains incompatible resource type 'rgbColors'."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -793,7 +801,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected data[] element for 'subscribers' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -837,7 +847,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected data[] element for 'tags' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index a3bf1986f2..03b36d589d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -588,7 +588,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected 'type' element in 'data' element."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -625,7 +627,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist. - Request body: <<"); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -661,7 +665,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().BeNull(); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -843,7 +849,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().Be("Changing the value of 'isImportant' is not allowed."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -884,7 +892,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().Be("Attribute 'isDeprecated' is read-only."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -914,7 +924,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().Match("Expected end of string, but instead reached end of data. * - Request body: <<*"); + error.Detail.Should().StartWith("Expected end of string, but instead reached end of data."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -955,7 +967,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Resource ID is read-only. - Request body: <<"); + error.Detail.Should().Be("Resource ID is read-only."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -995,7 +1009,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be($"Failed to convert ID '{existingWorkItem.Id}' of type 'Number' to type 'String'."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1040,9 +1056,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().Match("Failed to convert attribute 'dueAt' with value '*start*end*' of type 'Object' to type 'Nullable'."); - error.Detail.Should().Match("Failed to convert attribute 'dueAt' with value '*start*end*' " + - "of type 'Object' to type 'Nullable'. - Request body: <<*"); + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index 3eac83cc04..70365d1746 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -480,7 +480,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected 'type' element in 'assignee' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -528,7 +530,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist. - Request body: <<"); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -575,7 +579,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected 'id' element in 'assignee' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -673,7 +679,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Relationship 'assignee' contains incompatible resource type 'rgbColors'."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -725,7 +733,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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: <<"); + error.Detail.Should().Be("Expected single data element for 'assignee' relationship."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/TestBuildingBlocks/TestableStartup.cs b/test/TestBuildingBlocks/TestableStartup.cs index 3f029e4eb6..7b0c927645 100644 --- a/test/TestBuildingBlocks/TestableStartup.cs +++ b/test/TestBuildingBlocks/TestableStartup.cs @@ -18,6 +18,7 @@ public virtual void ConfigureServices(IServiceCollection services) protected virtual void SetJsonApiOptions(JsonApiOptions options) { options.IncludeExceptionStackTraceInErrors = true; + options.IncludeRequestBodyInErrors = true; options.SerializerOptions.WriteIndented = true; } From 2b47438469c7025d10547565d28016601abc7fdb Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 22 Sep 2021 13:38:00 +0200 Subject: [PATCH 04/49] Breaking: Added option to allow unknown attribute/relationship keys in request body (false by default) --- .../Configuration/IJsonApiOptions.cs | 5 + .../Configuration/JsonApiOptions.cs | 3 + .../Serialization/BaseDeserializer.cs | 33 ++++- .../JsonConverters/ResourceObjectConverter.cs | 3 + .../Serialization/RequestDeserializer.cs | 3 +- .../Creating/AtomicCreateResourceTests.cs | 103 +++++++++++++++ .../Resources/AtomicUpdateResourceTests.cs | 119 ++++++++++++++++++ .../ReadWrite/Creating/CreateResourceTests.cs | 87 ++++++++++++- .../Updating/Resources/UpdateResourceTests.cs | 105 ++++++++++++++++ .../DefaultBehaviorTests.cs | 2 +- .../Serialization/DeserializerTestsSetup.cs | 2 +- 11 files changed, 455 insertions(+), 10 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index 3318a71679..6e8e2d981e 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -118,6 +118,11 @@ public interface IJsonApiOptions /// bool AllowUnknownQueryStringParameters { get; } + /// + /// Whether or not to produce an error on unknown attribute and relationship keys in request bodies. False by default. + /// + bool AllowUnknownFieldsInRequestBody { get; } + /// /// Determines whether legacy filter notation in query strings, such as =eq:, =like:, and =in: is enabled. False by default. /// diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 05472aff5e..322e2cd725 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -74,6 +74,9 @@ public sealed class JsonApiOptions : IJsonApiOptions /// public bool AllowUnknownQueryStringParameters { get; set; } + /// + public bool AllowUnknownFieldsInRequestBody { get; set; } + /// public bool EnableLegacyFilterNotation { get; set; } diff --git a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs index dfba94691c..ab25eb2fe0 100644 --- a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs @@ -19,18 +19,21 @@ namespace JsonApiDotNetCore.Serialization public abstract class BaseDeserializer { private protected static readonly CollectionConverter CollectionConverter = new(); + private readonly IJsonApiOptions _options; protected IResourceGraph ResourceGraph { get; } protected IResourceFactory ResourceFactory { get; } protected int? AtomicOperationIndex { get; set; } - protected BaseDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory) + protected BaseDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory, IJsonApiOptions options) { ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); + ArgumentGuard.NotNull(options, nameof(options)); ResourceGraph = resourceGraph; ResourceFactory = resourceFactory; + _options = options; } /// @@ -123,6 +126,8 @@ private IIdentifiable SetAttributes(IIdentifiable resource, IDictionary /// Relationships and their values, as in the serialized content. /// - /// + /// /// Exposed relationships for . /// private IIdentifiable SetRelationships(IIdentifiable resource, IDictionary relationshipValues, - IReadOnlyCollection relationshipAttributes) + IReadOnlyCollection relationships) { ArgumentGuard.NotNull(resource, nameof(resource)); - ArgumentGuard.NotNull(relationshipAttributes, nameof(relationshipAttributes)); + ArgumentGuard.NotNull(relationships, nameof(relationships)); if (relationshipValues.IsNullOrEmpty()) { return resource; } - foreach (RelationshipAttribute attr in relationshipAttributes) + foreach (RelationshipAttribute attr in relationships) { bool relationshipIsProvided = relationshipValues.TryGetValue(attr.PublicName, out RelationshipObject relationshipData); + relationshipValues.Remove(attr.PublicName); + if (!relationshipIsProvided || !relationshipData.Data.IsAssigned) { continue; @@ -193,6 +208,14 @@ private IIdentifiable SetRelationships(IIdentifiable resource, IDictionary ReadAttributes(ref Utf8JsonReader rea } else { + attributes.Add(attributeName!, null); reader.Skip(); } diff --git a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs index 3b466a7087..592f389dcb 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs @@ -29,12 +29,11 @@ public class RequestDeserializer : BaseDeserializer, IJsonApiDeserializer public RequestDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory, ITargetedFields targetedFields, IHttpContextAccessor httpContextAccessor, IJsonApiRequest request, IJsonApiOptions options, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(resourceGraph, resourceFactory) + : base(resourceGraph, resourceFactory, options) { ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); _targetedFields = targetedFields; diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index 93dd55627e..2136fcb55d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -6,8 +6,10 @@ using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Extensions; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -28,6 +30,9 @@ public AtomicCreateResourceTests(IntegrationTestContext(); testContext.UseController(); testContext.UseController(); + + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownFieldsInRequestBody = false; } [Fact] @@ -211,10 +216,57 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); } + [Fact] + public async Task Cannot_create_resource_with_unknown_attribute() + { + // Arrange + string newName = _fakers.Playlist.Generate().Name; + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "add", + data = new + { + type = "playlists", + attributes = new + { + doesNotExist = "ignored", + name = newName + } + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: Request body includes unknown attribute."); + error.Detail.Should().Be("Attribute 'doesNotExist' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + [Fact] public async Task Can_create_resource_with_unknown_attribute() { // Arrange + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownFieldsInRequestBody = true; + string newName = _fakers.Playlist.Generate().Name; var requestBody = new @@ -261,10 +313,61 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); } + [Fact] + public async Task Cannot_create_resource_with_unknown_relationship() + { + // Arrange + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "add", + data = new + { + type = "lyrics", + relationships = new + { + doesNotExist = new + { + data = new + { + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 + } + } + } + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: Request body includes unknown relationship."); + error.Detail.Should().Be("Relationship 'doesNotExist' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + [Fact] public async Task Can_create_resource_with_unknown_relationship() { // Arrange + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownFieldsInRequestBody = true; + var requestBody = new { atomic__operations = new[] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 1a6b73e239..729413c8b1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -9,6 +9,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -32,6 +33,9 @@ public AtomicUpdateResourceTests(IntegrationTestContext(); }); + + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownFieldsInRequestBody = false; } [Fact] @@ -157,10 +161,65 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); } + [Fact] + public async Task Cannot_update_resource_with_unknown_attribute() + { + // Arrange + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + string newTitle = _fakers.MusicTrack.Generate().Title; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.MusicTracks.Add(existingTrack); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "update", + data = new + { + type = "musicTracks", + id = existingTrack.StringId, + attributes = new + { + title = newTitle, + doesNotExist = "Ignored" + } + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: Request body includes unknown attribute."); + error.Detail.Should().Be("Attribute 'doesNotExist' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + [Fact] public async Task Can_update_resource_with_unknown_attribute() { // Arrange + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownFieldsInRequestBody = true; + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); string newTitle = _fakers.MusicTrack.Generate().Title; @@ -209,10 +268,70 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); } + [Fact] + public async Task Cannot_update_resource_with_unknown_relationship() + { + // Arrange + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.MusicTracks.Add(existingTrack); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "update", + data = new + { + type = "musicTracks", + id = existingTrack.StringId, + relationships = new + { + doesNotExist = new + { + data = new + { + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 + } + } + } + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: Request body includes unknown relationship."); + error.Detail.Should().Be("Relationship 'doesNotExist' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + [Fact] public async Task Can_update_resource_with_unknown_relationship() { // Arrange + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownFieldsInRequestBody = true; + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 84d976a5d9..2835828069 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -33,6 +33,7 @@ public CreateResourceTests(IntegrationTestContext(); options.UseRelativeLinks = false; options.AllowClientGeneratedIds = false; + options.AllowUnknownFieldsInRequestBody = false; } [Fact] @@ -252,10 +253,50 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); } + [Fact] + public async Task Cannot_create_resource_with_unknown_attribute() + { + // Arrange + WorkItem newWorkItem = _fakers.WorkItem.Generate(); + + var requestBody = new + { + data = new + { + type = "workItems", + attributes = new + { + doesNotExist = "ignored", + description = newWorkItem.Description + } + } + }; + + const string route = "/workItems"; + + // 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: Request body includes unknown attribute."); + error.Detail.Should().Be("Attribute 'doesNotExist' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + [Fact] public async Task Can_create_resource_with_unknown_attribute() { // Arrange + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownFieldsInRequestBody = true; + WorkItem newWorkItem = _fakers.WorkItem.Generate(); var requestBody = new @@ -294,10 +335,54 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); } + [Fact] + public async Task Cannot_create_resource_with_unknown_relationship() + { + // Arrange + var requestBody = new + { + data = new + { + type = "workItems", + relationships = new + { + doesNotExist = new + { + data = new + { + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 + } + } + } + } + }; + + const string route = "/workItems"; + + // 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: Request body includes unknown relationship."); + error.Detail.Should().Be("Relationship 'doesNotExist' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + [Fact] public async Task Can_create_resource_with_unknown_relationship() { // Arrange + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownFieldsInRequestBody = true; + var requestBody = new { data = new @@ -352,7 +437,7 @@ public async Task Cannot_create_resource_with_client_generated_ID() id = "0A0B0C", attributes = new { - name = "Black" + displayName = "Black" } } }; diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 03b36d589d..32aed8dbb1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -10,6 +10,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -34,6 +35,9 @@ public UpdateResourceTests(IntegrationTestContext(); services.AddResourceDefinition(); }); + + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownFieldsInRequestBody = false; } [Fact] @@ -82,10 +86,58 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); } + [Fact] + public async Task Cannot_update_resource_with_unknown_attribute() + { + // Arrange + UserAccount existingUserAccount = _fakers.UserAccount.Generate(); + string newFirstName = _fakers.UserAccount.Generate().FirstName; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.UserAccounts.Add(existingUserAccount); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "userAccounts", + id = existingUserAccount.StringId, + attributes = new + { + firstName = newFirstName, + doesNotExist = "Ignored" + } + } + }; + + string route = $"/userAccounts/{existingUserAccount.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: Request body includes unknown attribute."); + error.Detail.Should().Be("Attribute 'doesNotExist' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + [Fact] public async Task Can_update_resource_with_unknown_attribute() { // Arrange + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownFieldsInRequestBody = true; + UserAccount existingUserAccount = _fakers.UserAccount.Generate(); string newFirstName = _fakers.UserAccount.Generate().FirstName; @@ -128,10 +180,63 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); } + [Fact] + public async Task Cannot_update_resource_with_unknown_relationship() + { + // Arrange + UserAccount existingUserAccount = _fakers.UserAccount.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.UserAccounts.Add(existingUserAccount); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "userAccounts", + id = existingUserAccount.StringId, + relationships = new + { + doesNotExist = new + { + data = new + { + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 + } + } + } + } + }; + + string route = $"/userAccounts/{existingUserAccount.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: Request body includes unknown relationship."); + error.Detail.Should().Be("Relationship 'doesNotExist' does not exist."); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + [Fact] public async Task Can_update_resource_with_unknown_relationship() { // Arrange + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownFieldsInRequestBody = true; + UserAccount existingUserAccount = _fakers.UserAccount.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs index 20448ea5db..b5c193d9bf 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs @@ -43,7 +43,7 @@ public async Task Cannot_create_dependent_side_of_required_ManyToOne_relationshi type = "orders", attributes = new { - order = order.Amount + amount = order.Amount } } }; diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs index 05fc33c886..5e1ae08546 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -101,7 +101,7 @@ protected sealed class TestDeserializer : BaseDeserializer private readonly IJsonApiOptions _options; public TestDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory, IJsonApiOptions options) - : base(resourceGraph, resourceFactory) + : base(resourceGraph, resourceFactory, options) { _options = options; } From 2b1c32bb47b904b1471bcf876571b27ccae42828 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 23 Sep 2021 18:33:57 +0200 Subject: [PATCH 05/49] Fixed invalid test --- .../ReadWrite/Updating/Resources/UpdateResourceTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 32aed8dbb1..3903e1e3e6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -963,11 +963,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_update_resource_with_readonly_attribute() { // Arrange - WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + WorkItemGroup existingWorkItemGroup = _fakers.WorkItemGroup.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.WorkItems.Add(existingWorkItem); + dbContext.Groups.Add(existingWorkItemGroup); await dbContext.SaveChangesAsync(); }); @@ -976,7 +976,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "workItemGroups", - id = existingWorkItem.StringId, + id = existingWorkItemGroup.StringId, attributes = new { isDeprecated = true @@ -984,7 +984,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/workItemGroups/{existingWorkItem.StringId}"; + string route = $"/workItemGroups/{existingWorkItemGroup.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); From 0192e3938a70007456f5a92aaf1384b2cbe56494 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 29 Sep 2021 10:55:21 +0200 Subject: [PATCH 06/49] Rewrite of reading the request body and converting it into models. The existing validation logic inside controllers, JsonApiReader and RequestDeserializer/BaseDeserializer has been consolidated into a set of adapters that delegate to each other, passing along the current state. The dependency on `HttpContext` has been removed and similar errors are now grouped, in preparation to unify them. Because in this commit we always track the location of errors and write them to `error.source.pointer`, the error messages can be unified and simplified in future commits. In this commit, I've gone through great lenghts to preserve the existing error messages in our tests as much as possible, while keeping all tests green. All error tests in ReadWrite and Operations now have assertions on the source pointer. Added tests for missing/invalid 'data' in resource requests. Removed outdated unit tests and added new one for handling attributes of various CLR types. Updated benchmark code. The synthetic BenchmarkDotNet reports it has become 3-7% slower (we're talking microseconds here) compared to the old code. But when running full requests with our pipeline-instrumentation turned on, the difference falls in the range of background noise and is thus unmeasurable. ``` BEFORE: Read request body ............................... 0:00:00:00.0000167 ... 1.08% } 167 + 434 = 601 JsonSerializer.Deserialize .................... 0:00:00:00.0000560 ... 3.62% + Deserializer.Build (single) ................... 0:00:00:00.0000434 ... 2.80% } AFTER: Read request body ............................... 0:00:00:00.0000511 ... 3.43% JsonSerializer.Deserialize .................... 0:00:00:00.0000537 ... 3.61% ``` --- .../DeserializationBenchmarkBase.cs | 122 ++++ .../OperationsDeserializationBenchmarks.cs | 285 ++++++++++ .../ResourceDeserializationBenchmarks.cs | 151 +++++ benchmarks/Program.cs | 4 +- .../JsonApiDeserializerBenchmarks.cs | 57 -- .../AtomicOperations/OperationsProcessor.cs | 4 +- src/JsonApiDotNetCore/CollectionExtensions.cs | 5 + .../JsonApiApplicationBuilder.cs | 13 +- .../Controllers/BaseJsonApiController.cs | 5 - .../BaseJsonApiOperationsController.cs | 24 +- .../Errors/InvalidRequestBodyException.cs | 10 +- ...ceIdInCreateResourceNotAllowedException.cs | 10 +- .../Middleware/JsonApiInputFormatter.cs | 2 +- .../Resources/ITargetedFields.cs | 11 +- .../Resources/TargetedFields.cs | 23 +- .../Serialization/BaseDeserializer.cs | 382 ------------- .../Serialization/DeserializationException.cs | 23 + .../Serialization/IJsonApiDeserializer.cs | 18 - .../Serialization/IJsonApiReader.cs | 9 +- .../Serialization/IJsonApiWriter.cs | 3 + .../Serialization/JsonApiReader.cs | 245 ++------ .../JsonApiSerializationException.cs | 24 - .../JsonObjectConverter.cs | 2 +- .../AtomicOperationObjectAdapter.cs | 165 ++++++ .../RequestAdapters/AtomicReferenceAdapter.cs | 69 +++ .../RequestAdapters/AtomicReferenceResult.cs | 28 + .../RequestAdapters/DocumentAdapter.cs | 41 ++ .../IAtomicOperationObjectAdapter.cs | 16 + .../IAtomicReferenceAdapter.cs | 16 + .../RequestAdapters/IDocumentAdapter.cs | 38 ++ .../IOperationResourceDataAdapter.cs | 16 + .../IOperationsDocumentAdapter.cs | 17 + .../IRelationshipDataAdapter.cs | 24 + .../RequestAdapters/IResourceDataAdapter.cs | 16 + .../IResourceDocumentAdapter.cs | 15 + .../IResourceIdentifierObjectAdapter.cs | 16 + .../RequestAdapters/IResourceObjectAdapter.cs | 19 + .../RequestAdapters/JsonElementConstraint.cs | 21 + .../OperationResourceDataAdapter.cs | 28 + .../OperationsDocumentAdapter.cs | 73 +++ .../RelationshipDataAdapter.cs | 146 +++++ .../RequestAdapters/RequestAdapterPosition.cs | 76 +++ .../RequestAdapters/RequestAdapterState.cs | 70 +++ .../RequestAdapters/ResourceDataAdapter.cs | 65 +++ .../ResourceDocumentAdapter.cs | 94 ++++ .../ResourceIdentifierObjectAdapter.cs | 26 + .../ResourceIdentityAdapter.cs | 216 ++++++++ .../ResourceIdentityRequirements.cs | 43 ++ .../RequestAdapters/ResourceObjectAdapter.cs | 181 ++++++ .../Serialization/RequestDeserializer.cs | 523 ------------------ src/JsonApiDotNetCore/TypeExtensions.cs | 2 +- .../Creating/AtomicCreateResourceTests.cs | 26 +- ...reateResourceWithClientGeneratedIdTests.cs | 8 +- ...eateResourceWithToManyRelationshipTests.cs | 12 +- ...reateResourceWithToOneRelationshipTests.cs | 10 +- .../Deleting/AtomicDeleteResourceTests.cs | 24 +- .../Mixed/AtomicRequestBodyTests.cs | 58 +- .../Mixed/MaximumOperationsPerRequestTests.cs | 2 +- .../AtomicAddToToManyRelationshipTests.cs | 61 +- ...AtomicRemoveFromToManyRelationshipTests.cs | 59 +- .../AtomicReplaceToManyRelationshipTests.cs | 60 +- .../AtomicUpdateToOneRelationshipTests.cs | 62 +-- .../AtomicReplaceToManyRelationshipTests.cs | 12 +- .../Resources/AtomicUpdateResourceTests.cs | 68 +-- .../AtomicUpdateToOneRelationshipTests.cs | 14 +- .../IntegrationTests/CompositeKeys/Car.cs | 18 +- .../IdObfuscation/ObfuscatedIdentifiable.cs | 4 +- .../NonJsonApiController.cs | 6 +- .../ReadWrite/Creating/CreateResourceTests.cs | 81 ++- ...reateResourceWithClientGeneratedIdTests.cs | 1 + ...eateResourceWithToManyRelationshipTests.cs | 7 + ...reateResourceWithToOneRelationshipTests.cs | 7 + .../ReadWrite/Deleting/DeleteResourceTests.cs | 1 + .../AddToToManyRelationshipTests.cs | 20 +- .../RemoveFromToManyRelationshipTests.cs | 19 +- .../ReplaceToManyRelationshipTests.cs | 21 +- .../UpdateToOneRelationshipTests.cs | 17 +- .../ReplaceToManyRelationshipTests.cs | 6 + .../Updating/Resources/UpdateResourceTests.cs | 109 +++- .../Resources/UpdateToOneRelationshipTests.cs | 6 + .../Serialization/InputConversionTests.cs | 352 ++++++++++++ .../IntegrationTestContext.cs | 2 +- .../Models/ResourceConstructionTests.cs | 132 ----- .../NeverResourceDefinitionAccessor.cs | 104 ---- .../Common/BaseDocumentParserTests.cs | 451 --------------- .../Serialization/DeserializerTestsSetup.cs | 119 ---- .../Server/RequestDeserializerTests.cs | 105 ---- test/UnitTests/TestResourceFactory.cs | 25 - 88 files changed, 3104 insertions(+), 2477 deletions(-) create mode 100644 benchmarks/Deserialization/DeserializationBenchmarkBase.cs create mode 100644 benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs create mode 100644 benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs delete mode 100644 benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs create mode 100644 src/JsonApiDotNetCore/Serialization/DeserializationException.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/JsonApiSerializationException.cs rename src/JsonApiDotNetCore/Serialization/{ => JsonConverters}/JsonObjectConverter.cs (95%) create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceResult.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicOperationObjectAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicReferenceAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationResourceDataAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationsDocumentAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/IRelationshipDataAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDocumentAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceIdentifierObjectAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceObjectAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/JsonElementConstraint.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationResourceDataAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterPosition.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterState.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDocumentAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentifierObjectAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityRequirements.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs create mode 100644 test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs delete mode 100644 test/UnitTests/Models/ResourceConstructionTests.cs delete mode 100644 test/UnitTests/NeverResourceDefinitionAccessor.cs delete mode 100644 test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs delete mode 100644 test/UnitTests/Serialization/DeserializerTestsSetup.cs delete mode 100644 test/UnitTests/Serialization/Server/RequestDeserializerTests.cs delete mode 100644 test/UnitTests/TestResourceFactory.cs diff --git a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs new file mode 100644 index 0000000000..191eb0a57a --- /dev/null +++ b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Design; +using System.Text.Json; +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.JsonConverters; +using JsonApiDotNetCore.Serialization.RequestAdapters; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Benchmarks.Deserialization +{ + public abstract class DeserializationBenchmarkBase + { + protected readonly JsonSerializerOptions SerializerReadOptions; + protected readonly DocumentAdapter DocumentAdapter; + + protected DeserializationBenchmarkBase() + { + var options = new JsonApiOptions(); + IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); + options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph)); + SerializerReadOptions = ((IJsonApiOptions)options).SerializerReadOptions; + + var serviceContainer = new ServiceContainer(); + var resourceFactory = new ResourceFactory(serviceContainer); + var resourceDefinitionAccessor = new ResourceDefinitionAccessor(resourceGraph, serviceContainer); + + serviceContainer.AddService(typeof(IResourceDefinitionAccessor), resourceDefinitionAccessor); + serviceContainer.AddService(typeof(IResourceDefinition), new JsonApiResourceDefinition(resourceGraph)); + + // ReSharper disable once VirtualMemberCallInConstructor + JsonApiRequest request = CreateJsonApiRequest(resourceGraph); + var targetedFields = new TargetedFields(); + + var resourceIdentifierObjectAdapter = new ResourceIdentifierObjectAdapter(resourceGraph, resourceFactory); + var relationshipDataAdapter = new RelationshipDataAdapter(resourceGraph, resourceIdentifierObjectAdapter); + var resourceObjectAdapter = new ResourceObjectAdapter(resourceGraph, resourceFactory, options, relationshipDataAdapter); + var resourceDataAdapter = new ResourceDataAdapter(resourceDefinitionAccessor, resourceObjectAdapter); + + var atomicReferenceAdapter = new AtomicReferenceAdapter(resourceGraph, resourceFactory); + var atomicOperationResourceDataAdapter = new OperationResourceDataAdapter(resourceDefinitionAccessor, resourceObjectAdapter); + + var atomicOperationObjectAdapter = new AtomicOperationObjectAdapter(resourceGraph, options, atomicReferenceAdapter, + atomicOperationResourceDataAdapter, relationshipDataAdapter); + + var resourceDocumentAdapter = new ResourceDocumentAdapter(options, resourceDataAdapter, relationshipDataAdapter); + var operationsDocumentAdapter = new OperationsDocumentAdapter(options, atomicOperationObjectAdapter); + + DocumentAdapter = new DocumentAdapter(request, targetedFields, resourceDocumentAdapter, operationsDocumentAdapter); + } + + protected abstract JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph); + + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class ResourceA : Identifiable + { + [Attr] + public bool Attribute01 { get; set; } + + [Attr] + public char Attribute02 { get; set; } + + [Attr] + public ulong? Attribute03 { get; set; } + + [Attr] + public decimal Attribute04 { get; set; } + + [Attr] + public float? Attribute05 { get; set; } + + [Attr] + public string Attribute06 { get; set; } + + [Attr] + public DateTime? Attribute07 { get; set; } + + [Attr] + public DateTimeOffset? Attribute08 { get; set; } + + [Attr] + public TimeSpan? Attribute09 { get; set; } + + [Attr] + public DayOfWeek Attribute10 { get; set; } + + [HasOne] + public ResourceA Single1 { get; set; } + + [HasOne] + public ResourceA Single2 { get; set; } + + [HasOne] + public ResourceA Single3 { get; set; } + + [HasOne] + public ResourceA Single4 { get; set; } + + [HasOne] + public ResourceA Single5 { get; set; } + + [HasMany] + public ISet Multi1 { get; set; } + + [HasMany] + public ISet Multi2 { get; set; } + + [HasMany] + public ISet Multi3 { get; set; } + + [HasMany] + public ISet Multi4 { get; set; } + + [HasMany] + public ISet Multi5 { get; set; } + } + } +} diff --git a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs new file mode 100644 index 0000000000..c09b7c77c7 --- /dev/null +++ b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs @@ -0,0 +1,285 @@ +using System; +using System.Text.Json; +using BenchmarkDotNet.Attributes; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; + +namespace Benchmarks.Deserialization +{ + [MarkdownExporter] + // ReSharper disable once ClassCanBeSealed.Global + public class OperationsDeserializationBenchmarks : DeserializationBenchmarkBase + { + private static readonly string RequestBody = JsonSerializer.Serialize(new + { + atomic__operations = new object[] + { + new + { + op = "add", + data = new + { + type = "resourceAs", + lid = "a-1", + attributes = new + { + attribute01 = true, + attribute02 = 'A', + attribute03 = 100UL, + attribute04 = 100.001m, + attribute05 = 200.002f, + attribute06 = "text", + attribute07 = DateTime.MaxValue, + attribute08 = DateTimeOffset.MaxValue, + attribute09 = TimeSpan.MaxValue, + attribute10 = DayOfWeek.Friday + }, + relationships = new + { + single1 = new + { + data = new + { + type = "resourceAs", + id = "101" + } + }, + single2 = new + { + data = new + { + type = "resourceAs", + id = "102" + } + }, + single3 = new + { + data = new + { + type = "resourceAs", + id = "103" + } + }, + single4 = new + { + data = new + { + type = "resourceAs", + id = "104" + } + }, + single5 = new + { + data = new + { + type = "resourceAs", + id = "105" + } + }, + multi1 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "201" + } + } + }, + multi2 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "202" + } + } + }, + multi3 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "203" + } + } + }, + multi4 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "204" + } + } + }, + multi5 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "205" + } + } + } + } + } + }, + new + { + op = "update", + data = new + { + type = "resourceAs", + id = "1", + attributes = new + { + attribute01 = true, + attribute02 = 'A', + attribute03 = 100UL, + attribute04 = 100.001m, + attribute05 = 200.002f, + attribute06 = "text", + attribute07 = DateTime.MaxValue, + attribute08 = DateTimeOffset.MaxValue, + attribute09 = TimeSpan.MaxValue, + attribute10 = DayOfWeek.Friday + }, + relationships = new + { + single1 = new + { + data = new + { + type = "resourceAs", + id = "101" + } + }, + single2 = new + { + data = new + { + type = "resourceAs", + id = "102" + } + }, + single3 = new + { + data = new + { + type = "resourceAs", + id = "103" + } + }, + single4 = new + { + data = new + { + type = "resourceAs", + id = "104" + } + }, + single5 = new + { + data = new + { + type = "resourceAs", + id = "105" + } + }, + multi1 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "201" + } + } + }, + multi2 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "202" + } + } + }, + multi3 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "203" + } + } + }, + multi4 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "204" + } + } + }, + multi5 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "205" + } + } + } + } + } + }, + new + { + op = "remove", + @ref = new + { + type = "resourceAs", + lid = "a-1" + } + } + } + }).Replace("atomic__operations", "atomic:operations"); + + [Benchmark] + public object DeserializeOperationsRequest() + { + var document = JsonSerializer.Deserialize(RequestBody, SerializerReadOptions); + return DocumentAdapter.Convert(document); + } + + protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) + { + return new() + { + Kind = EndpointKind.AtomicOperations + }; + } + } +} diff --git a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs new file mode 100644 index 0000000000..d603cd744a --- /dev/null +++ b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs @@ -0,0 +1,151 @@ +using System; +using System.Text.Json; +using BenchmarkDotNet.Attributes; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; + +namespace Benchmarks.Deserialization +{ + [MarkdownExporter] + // ReSharper disable once ClassCanBeSealed.Global + public class ResourceDeserializationBenchmarks : DeserializationBenchmarkBase + { + private static readonly string RequestBody = JsonSerializer.Serialize(new + { + data = new + { + type = "resourceAs", + attributes = new + { + attribute01 = true, + attribute02 = 'A', + attribute03 = 100UL, + attribute04 = 100.001m, + attribute05 = 200.002f, + attribute06 = "text", + attribute07 = DateTime.MaxValue, + attribute08 = DateTimeOffset.MaxValue, + attribute09 = TimeSpan.MaxValue, + attribute10 = DayOfWeek.Friday + }, + relationships = new + { + single1 = new + { + data = new + { + type = "resourceAs", + id = "101" + } + }, + single2 = new + { + data = new + { + type = "resourceAs", + id = "102" + } + }, + single3 = new + { + data = new + { + type = "resourceAs", + id = "103" + } + }, + single4 = new + { + data = new + { + type = "resourceAs", + id = "104" + } + }, + single5 = new + { + data = new + { + type = "resourceAs", + id = "105" + } + }, + multi1 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "201" + } + } + }, + multi2 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "202" + } + } + }, + multi3 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "203" + } + } + }, + multi4 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "204" + } + } + }, + multi5 = new + { + data = new[] + { + new + { + type = "resourceAs", + id = "205" + } + } + } + } + } + }); + + [Benchmark] + public object DeserializeResourceRequest() + { + var document = JsonSerializer.Deserialize(RequestBody, SerializerReadOptions); + + return DocumentAdapter.Convert(document); + } + + protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) + { + return new() + { + Kind = EndpointKind.Primary, + PrimaryResource = resourceGraph.GetResourceContext(), + WriteOperation = WriteOperationKind.CreateResource + }; + } + } +} diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs index 0d745a795d..5a36949b9b 100644 --- a/benchmarks/Program.cs +++ b/benchmarks/Program.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Running; +using Benchmarks.Deserialization; using Benchmarks.LinkBuilder; using Benchmarks.Query; using Benchmarks.Serialization; @@ -11,7 +12,8 @@ private static void Main(string[] args) { var switcher = new BenchmarkSwitcher(new[] { - typeof(JsonApiDeserializerBenchmarks), + typeof(ResourceDeserializationBenchmarks), + typeof(OperationsDeserializationBenchmarks), typeof(JsonApiSerializerBenchmarks), typeof(QueryParserBenchmarks), typeof(LinkBuilderGetNamespaceFromPathBenchmarks) diff --git a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs deleted file mode 100644 index 2c2cb62223..0000000000 --- a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.ComponentModel.Design; -using System.Text.Json; -using BenchmarkDotNet.Attributes; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization; -using Microsoft.AspNetCore.Http; - -namespace Benchmarks.Serialization -{ - // ReSharper disable once ClassCanBeSealed.Global - [MarkdownExporter] - public class JsonApiDeserializerBenchmarks - { - private static readonly string RequestBody = JsonSerializer.Serialize(new - { - data = new - { - type = BenchmarkResourcePublicNames.Type, - id = "1", - attributes = new - { - } - } - }); - - private readonly DependencyFactory _dependencyFactory = new(); - private readonly IJsonApiDeserializer _jsonApiDeserializer; - - public JsonApiDeserializerBenchmarks() - { - var options = new JsonApiOptions(); - IResourceGraph resourceGraph = _dependencyFactory.CreateResourceGraph(options); - - var serviceContainer = new ServiceContainer(); - var resourceDefinitionAccessor = new ResourceDefinitionAccessor(resourceGraph, serviceContainer); - - serviceContainer.AddService(typeof(IResourceDefinitionAccessor), resourceDefinitionAccessor); - serviceContainer.AddService(typeof(IResourceDefinition), new JsonApiResourceDefinition(resourceGraph)); - - var targetedFields = new TargetedFields(); - var request = new JsonApiRequest(); - var resourceFactory = new ResourceFactory(serviceContainer); - var httpContextAccessor = new HttpContextAccessor(); - - _jsonApiDeserializer = new RequestDeserializer(resourceGraph, resourceFactory, targetedFields, httpContextAccessor, request, options, - resourceDefinitionAccessor); - } - - [Benchmark] - public object DeserializeSimpleObject() - { - return _jsonApiDeserializer.Deserialize(RequestBody); - } - } -} diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs index 8d531ea231..9a142c0a4f 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs @@ -109,9 +109,7 @@ protected virtual async Task ProcessOperationAsync(Operation TrackLocalIdsForOperation(operation); - _targetedFields.Attributes = operation.TargetedFields.Attributes; - _targetedFields.Relationships = operation.TargetedFields.Relationships; - + _targetedFields.CopyFrom(operation.TargetedFields); _request.CopyFrom(operation.Request); return await _operationProcessorAccessor.ProcessAsync(operation, cancellationToken); diff --git a/src/JsonApiDotNetCore/CollectionExtensions.cs b/src/JsonApiDotNetCore/CollectionExtensions.cs index ca69755c1c..863b22d5fd 100644 --- a/src/JsonApiDotNetCore/CollectionExtensions.cs +++ b/src/JsonApiDotNetCore/CollectionExtensions.cs @@ -71,6 +71,11 @@ public static bool DictionaryEqual(this IReadOnlyDictionary EmptyIfNull(this IEnumerable source) + { + return source ?? Enumerable.Empty(); + } + public static void AddRange(this ICollection source, IEnumerable itemsToAdd) { ArgumentGuard.NotNull(source, nameof(source)); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 9200149b25..996030fe08 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -13,6 +13,7 @@ using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.JsonConverters; +using JsonApiDotNetCore.Serialization.RequestAdapters; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -251,7 +252,6 @@ private void RegisterDependentService() private void AddSerializationLayer() { _services.AddScoped(); - _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); @@ -262,6 +262,17 @@ private void AddSerializationLayer() _services.AddScoped(); _services.AddSingleton(); _services.AddSingleton(); + + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); } private void AddOperationsLayer() diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index 98a4c58afe..90a8f3078f 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -182,11 +182,6 @@ public virtual async Task PostAsync([FromBody] TResource resource throw new RequestMethodNotAllowedException(HttpMethod.Post); } - if (!_options.AllowClientGeneratedIds && resource.StringId != null) - { - throw new ResourceIdInCreateResourceNotAllowedException(); - } - if (_options.ValidateModelState && !ModelState.IsValid) { throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs index 0ed536eb15..4fb78e8515 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs @@ -113,8 +113,6 @@ public virtual async Task PostOperationsAsync([FromBody] IList PostOperationsAsync([FromBody] IList result != null) ? Ok(results) : NoContent(); } - protected virtual void ValidateClientGeneratedIds(IEnumerable operations) - { - if (!_options.AllowClientGeneratedIds) - { - int index = 0; - - foreach (OperationContainer operation in operations) - { - if (operation.Kind == WriteOperationKind.CreateResource && operation.Resource.StringId != null) - { - throw new ResourceIdInCreateResourceNotAllowedException(index); - } - - index++; - } - } - } - protected virtual void ValidateModelState(IEnumerable operations) { // We must validate the resource inside each operation manually, because they are typed as IIdentifiable. @@ -155,9 +135,7 @@ protected virtual void ValidateModelState(IEnumerable operat { if (operation.Kind == WriteOperationKind.CreateResource || operation.Kind == WriteOperationKind.UpdateResource) { - _targetedFields.Attributes = operation.TargetedFields.Attributes; - _targetedFields.Relationships = operation.TargetedFields.Relationships; - + _targetedFields.CopyFrom(operation.TargetedFields); _request.CopyFrom(operation.Request); var validationContext = new ActionContext(); diff --git a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs index 1d53ca3635..ede05c3eb4 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs @@ -14,11 +14,17 @@ public sealed class InvalidRequestBodyException : JsonApiException { public string RequestBody { get; } - public InvalidRequestBodyException(string reason, string details, string requestBody, Exception innerException = null) + public InvalidRequestBodyException(string reason, string details, string requestBody, string sourcePointer, Exception innerException = null) : base(new ErrorObject(HttpStatusCode.UnprocessableEntity) { Title = reason != null ? $"Failed to deserialize request body: {reason}" : "Failed to deserialize request body.", - Detail = FormatErrorDetail(details, innerException) + Detail = FormatErrorDetail(details, innerException), + Source = sourcePointer == null + ? null + : new ErrorSource + { + Pointer = sourcePointer + } }, innerException) { RequestBody = requestBody; diff --git a/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs index 11a96cc436..a1efb23755 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs @@ -10,15 +10,15 @@ namespace JsonApiDotNetCore.Errors [PublicAPI] public sealed class ResourceIdInCreateResourceNotAllowedException : JsonApiException { - public ResourceIdInCreateResourceNotAllowedException(int? atomicOperationIndex = null) + public ResourceIdInCreateResourceNotAllowedException(bool isOperationsRequest, string sourcePointer) : 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.", + Title = isOperationsRequest + ? "Specifying the resource ID in operations that create a resource is not allowed." + : "Specifying the resource ID in POST resource requests is not allowed.", Source = new ErrorSource { - Pointer = atomicOperationIndex != null ? $"/atomic:operations[{atomicOperationIndex}]/data/id" : "/data/id" + Pointer = sourcePointer } }) { diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs index fc5a1e2230..9672be0dea 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs @@ -22,7 +22,7 @@ public async Task ReadAsync(InputFormatterContext context) ArgumentGuard.NotNull(context, nameof(context)); var reader = context.HttpContext.RequestServices.GetRequiredService(); - return await reader.ReadAsync(context); + return await reader.ReadAsync(context.HttpContext.Request); } } } diff --git a/src/JsonApiDotNetCore/Resources/ITargetedFields.cs b/src/JsonApiDotNetCore/Resources/ITargetedFields.cs index 5cdb36950d..1498f96744 100644 --- a/src/JsonApiDotNetCore/Resources/ITargetedFields.cs +++ b/src/JsonApiDotNetCore/Resources/ITargetedFields.cs @@ -4,18 +4,23 @@ namespace JsonApiDotNetCore.Resources { /// - /// Container to register which resource attributes and relationships are targeted by a request. + /// Container to register which resource fields (attributes and relationships) are targeted by a request. /// public interface ITargetedFields { /// /// The set of attributes that are targeted by a request. /// - ISet Attributes { get; set; } + IReadOnlySet Attributes { get; } /// /// The set of relationships that are targeted by a request. /// - ISet Relationships { get; set; } + IReadOnlySet Relationships { get; } + + /// + /// Performs a shallow copy. + /// + void CopyFrom(ITargetedFields other); } } diff --git a/src/JsonApiDotNetCore/Resources/TargetedFields.cs b/src/JsonApiDotNetCore/Resources/TargetedFields.cs index 46cd2fed6a..4e2d571e5c 100644 --- a/src/JsonApiDotNetCore/Resources/TargetedFields.cs +++ b/src/JsonApiDotNetCore/Resources/TargetedFields.cs @@ -1,15 +1,32 @@ using System.Collections.Generic; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Resources { /// + [PublicAPI] public sealed class TargetedFields : ITargetedFields { - /// - public ISet Attributes { get; set; } = new HashSet(); + IReadOnlySet ITargetedFields.Attributes => Attributes; + IReadOnlySet ITargetedFields.Relationships => Relationships; + + public HashSet Attributes { get; } = new(); + public HashSet Relationships { get; } = new(); /// - public ISet Relationships { get; set; } = new HashSet(); + public void CopyFrom(ITargetedFields other) + { + Clear(); + + Attributes.AddRange(other.Attributes); + Relationships.AddRange(other.Relationships); + } + + public void Clear() + { + Attributes.Clear(); + Relationships.Clear(); + } } } diff --git a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs deleted file mode 100644 index ab25eb2fe0..0000000000 --- a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs +++ /dev/null @@ -1,382 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -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.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// Abstract base class for deserialization. Deserializes JSON content into s and constructs instances of the resource(s) - /// in the document body. - /// - [PublicAPI] - public abstract class BaseDeserializer - { - private protected static readonly CollectionConverter CollectionConverter = new(); - private readonly IJsonApiOptions _options; - - protected IResourceGraph ResourceGraph { get; } - protected IResourceFactory ResourceFactory { get; } - protected int? AtomicOperationIndex { get; set; } - - protected BaseDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory, IJsonApiOptions options) - { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); - ArgumentGuard.NotNull(options, nameof(options)); - - ResourceGraph = resourceGraph; - ResourceFactory = resourceFactory; - _options = options; - } - - /// - /// This method is called each time a is constructed from the serialized content, which is used to do additional processing - /// depending on the type of deserializer. - /// - /// - /// See the implementation of this method in for usage. - /// - /// - /// The resource that was constructed from the document's body. - /// - /// - /// The metadata for the exposed field. - /// - /// - /// Relationship data for . Is null when is not a . - /// - protected abstract void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipObject data = null); - - protected Document DeserializeDocument(string body, JsonSerializerOptions serializerOptions) - { - ArgumentGuard.NotNullNorEmpty(body, nameof(body)); - - try - { - 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.Data.ManyValue != null) - { - using (CodeTimingSessionManager.Current.Measure("Deserializer.Build (list)")) - { - return document.Data.ManyValue.Select(ParseResourceObject).ToHashSet(IdentifiableComparer.Instance); - } - } - - if (document.Data.SingleValue != null) - { - using (CodeTimingSessionManager.Current.Measure("Deserializer.Build (single)")) - { - return ParseResourceObject(document.Data.SingleValue); - } - } - } - - return null; - } - - /// - /// Sets the attributes on a parsed resource. - /// - /// - /// The parsed resource. - /// - /// - /// Attributes and their values, as in the serialized content. - /// - /// - /// Exposed attributes for . - /// - private IIdentifiable SetAttributes(IIdentifiable resource, IDictionary attributeValues, IReadOnlyCollection attributes) - { - ArgumentGuard.NotNull(resource, nameof(resource)); - ArgumentGuard.NotNull(attributes, nameof(attributes)); - - if (attributeValues.IsNullOrEmpty()) - { - return resource; - } - - foreach (AttrAttribute attr in attributes) - { - if (attributeValues.TryGetValue(attr.PublicName, out object newValue)) - { - attributeValues.Remove(attr.PublicName); - - if (attr.Property.SetMethod == null) - { - throw new JsonApiSerializationException("Attribute is read-only.", $"Attribute '{attr.PublicName}' is read-only.", - atomicOperationIndex: AtomicOperationIndex); - } - - if (newValue is JsonInvalidAttributeInfo info) - { - if (newValue == JsonInvalidAttributeInfo.Id) - { - throw new JsonApiSerializationException(null, "Resource ID is read-only.", atomicOperationIndex: AtomicOperationIndex); - } - - string typeName = info.AttributeType.GetFriendlyTypeName(); - - throw new JsonApiSerializationException(null, - $"Failed to convert attribute '{info.AttributeName}' with value '{info.JsonValue}' of type '{info.JsonType}' to type '{typeName}'.", - atomicOperationIndex: AtomicOperationIndex); - } - - attr.SetValue(resource, newValue); - AfterProcessField(resource, attr); - } - } - - if (!_options.AllowUnknownFieldsInRequestBody && attributeValues.Any()) - { - string attributeName = attributeValues.First().Key; - - throw new JsonApiSerializationException("Request body includes unknown attribute.", $"Attribute '{attributeName}' does not exist.", - atomicOperationIndex: AtomicOperationIndex); - } - - return resource; - } - - /// - /// Sets the relationships on a parsed resource. - /// - /// - /// The parsed resource. - /// - /// - /// Relationships and their values, as in the serialized content. - /// - /// - /// Exposed relationships for . - /// - private IIdentifiable SetRelationships(IIdentifiable resource, IDictionary relationshipValues, - IReadOnlyCollection relationships) - { - ArgumentGuard.NotNull(resource, nameof(resource)); - ArgumentGuard.NotNull(relationships, nameof(relationships)); - - if (relationshipValues.IsNullOrEmpty()) - { - return resource; - } - - foreach (RelationshipAttribute attr in relationships) - { - bool relationshipIsProvided = relationshipValues.TryGetValue(attr.PublicName, out RelationshipObject relationshipData); - - relationshipValues.Remove(attr.PublicName); - - if (!relationshipIsProvided || !relationshipData.Data.IsAssigned) - { - continue; - } - - if (attr is HasOneAttribute hasOneAttribute) - { - SetHasOneRelationship(resource, hasOneAttribute, relationshipData); - } - else if (attr is HasManyAttribute hasManyAttribute) - { - SetHasManyRelationship(resource, hasManyAttribute, relationshipData); - } - } - - if (!_options.AllowUnknownFieldsInRequestBody && relationshipValues.Any()) - { - string relationshipName = relationshipValues.First().Key; - - throw new JsonApiSerializationException("Request body includes unknown relationship.", $"Relationship '{relationshipName}' does not exist.", - atomicOperationIndex: AtomicOperationIndex); - } - - return resource; - } - - /// - /// Creates an instance of the referenced type in and sets its attributes and relationships. - /// - /// - /// The parsed resource. - /// - protected IIdentifiable ParseResourceObject(ResourceObject data) - { - AssertHasType(data, null); - - if (AtomicOperationIndex == null) - { - AssertHasNoLid(data); - } - - ResourceContext resourceContext = GetExistingResourceContext(data.Type); - IIdentifiable resource = ResourceFactory.CreateInstance(resourceContext.ResourceType); - - resource = SetAttributes(resource, data.Attributes, resourceContext.Attributes); - resource = SetRelationships(resource, data.Relationships, resourceContext.Relationships); - - if (data.Id != null) - { - resource.StringId = data.Id; - } - - resource.LocalId = data.Lid; - - return resource; - } - - protected ResourceContext GetExistingResourceContext(string publicName) - { - ResourceContext resourceContext = ResourceGraph.TryGetResourceContext(publicName); - - if (resourceContext == null) - { - throw new JsonApiSerializationException("Request body includes unknown resource type.", $"Resource type '{publicName}' does not exist.", - atomicOperationIndex: AtomicOperationIndex); - } - - return resourceContext; - } - - /// - /// Sets a HasOne relationship on a parsed resource. - /// - private void SetHasOneRelationship(IIdentifiable resource, HasOneAttribute hasOneRelationship, RelationshipObject relationshipData) - { - 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.Data.SingleValue); - hasOneRelationship.SetValue(resource, rightResource); - - // depending on if this base parser is used client-side or server-side, - // different additional processing per field needs to be executed. - AfterProcessField(resource, hasOneRelationship, relationshipData); - } - - /// - /// Sets a HasMany relationship. - /// - private void SetHasManyRelationship(IIdentifiable resource, HasManyAttribute hasManyRelationship, RelationshipObject relationshipData) - { - 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.Data.ManyValue.Select(rio => CreateRightResource(hasManyRelationship, rio)) - .ToHashSet(IdentifiableComparer.Instance); - - IEnumerable convertedCollection = CollectionConverter.CopyToTypedCollection(rightResources, hasManyRelationship.Property.PropertyType); - hasManyRelationship.SetValue(resource, convertedCollection); - - AfterProcessField(resource, hasManyRelationship, relationshipData); - } - - private IIdentifiable CreateRightResource(RelationshipAttribute relationship, ResourceIdentifierObject resourceIdentifierObject) - { - if (resourceIdentifierObject != null) - { - AssertHasType(resourceIdentifierObject, relationship); - AssertHasIdOrLid(resourceIdentifierObject, relationship); - - ResourceContext rightResourceContext = GetExistingResourceContext(resourceIdentifierObject.Type); - AssertRightTypeIsCompatible(rightResourceContext, relationship); - - IIdentifiable rightInstance = ResourceFactory.CreateInstance(rightResourceContext.ResourceType); - rightInstance.StringId = resourceIdentifierObject.Id; - rightInstance.LocalId = resourceIdentifierObject.Lid; - - return rightInstance; - } - - return null; - } - - [AssertionMethod] - private void AssertHasType(IResourceIdentity resourceIdentity, RelationshipAttribute relationship) - { - if (resourceIdentity.Type == null) - { - string details = relationship != null - ? $"Expected 'type' element in '{relationship.PublicName}' relationship." - : "Expected 'type' element in 'data' element."; - - throw new JsonApiSerializationException("Request body must include 'type' element.", details, atomicOperationIndex: AtomicOperationIndex); - } - } - - private void AssertHasIdOrLid(ResourceIdentifierObject resourceIdentifierObject, RelationshipAttribute relationship) - { - if (AtomicOperationIndex != null) - { - bool hasNone = resourceIdentifierObject.Id == null && resourceIdentifierObject.Lid == null; - bool hasBoth = resourceIdentifierObject.Id != null && resourceIdentifierObject.Lid != null; - - if (hasNone || hasBoth) - { - throw new JsonApiSerializationException("Request body must include 'id' or 'lid' element.", - $"Expected 'id' or 'lid' element in '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); - } - } - else - { - if (resourceIdentifierObject.Id == null) - { - throw new JsonApiSerializationException("Request body must include 'id' element.", - $"Expected 'id' element in '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); - } - - AssertHasNoLid(resourceIdentifierObject); - } - } - - [AssertionMethod] - private void AssertHasNoLid(IResourceIdentity resourceIdentityObject) - { - if (resourceIdentityObject.Lid != null) - { - throw new JsonApiSerializationException(null, "Local IDs cannot be used at this endpoint.", atomicOperationIndex: AtomicOperationIndex); - } - } - - private void AssertRightTypeIsCompatible(ResourceContext rightResourceContext, RelationshipAttribute relationship) - { - if (!relationship.RightType.IsAssignableFrom(rightResourceContext.ResourceType)) - { - throw new JsonApiSerializationException("Relationship contains incompatible resource type.", - $"Relationship '{relationship.PublicName}' contains incompatible resource type '{rightResourceContext.PublicName}'.", - atomicOperationIndex: AtomicOperationIndex); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/DeserializationException.cs b/src/JsonApiDotNetCore/Serialization/DeserializationException.cs new file mode 100644 index 0000000000..49402c2f1b --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/DeserializationException.cs @@ -0,0 +1,23 @@ +using System; +using JsonApiDotNetCore.Serialization.RequestAdapters; + +namespace JsonApiDotNetCore.Serialization +{ + /// + /// The error that is thrown when deserialization of a JSON:API request body fails. + /// + internal sealed class DeserializationException : Exception + { + public string GenericMessage { get; } + public string SpecificMessage { get; } + public string SourcePointer { get; } + + public DeserializationException(RequestAdapterPosition position, string genericMessage, string specificMessage) + : base(genericMessage) + { + GenericMessage = genericMessage; + SpecificMessage = specificMessage; + SourcePointer = position?.ToSourcePointer(); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs deleted file mode 100644 index ea392575b9..0000000000 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs +++ /dev/null @@ -1,18 +0,0 @@ -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// Deserializer used internally in JsonApiDotNetCore to deserialize requests. - /// - public interface IJsonApiDeserializer - { - /// - /// Deserializes JSON into a and constructs resources from the 'data' element. - /// - /// - /// The JSON to be deserialized. - /// - object Deserialize(string body); - } -} diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs index dbc851a492..5ae602f6e0 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs @@ -1,15 +1,20 @@ using System.Threading.Tasks; using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; namespace JsonApiDotNetCore.Serialization { /// - /// The deserializer of the body, used in ASP.NET Core internally to process `FromBody`. + /// Deserializes the incoming JSON request body and converts it to models, which are passed to controller actions by ASP.NET Core on `FromBody` + /// parameters. /// [PublicAPI] public interface IJsonApiReader { - Task ReadAsync(InputFormatterContext context); + /// + /// Reads an object from the request body. + /// + Task ReadAsync(HttpRequest httpRequest); } } diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs index ac29395115..52cc6713e1 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs @@ -4,6 +4,9 @@ namespace JsonApiDotNetCore.Serialization { + /// + /// Serializes models into the outgoing JSON response body. + /// [PublicAPI] public interface IJsonApiWriter { diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index df78c648d0..2f2fa4faaa 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -1,19 +1,15 @@ using System; -using System.Collections; -using System.Collections.Generic; 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.Errors; using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.Serialization.RequestAdapters; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc.Formatters; @@ -25,126 +21,64 @@ namespace JsonApiDotNetCore.Serialization [PublicAPI] public class JsonApiReader : IJsonApiReader { - private readonly IJsonApiDeserializer _deserializer; - private readonly IJsonApiRequest _request; - private readonly IResourceGraph _resourceGraph; + private readonly IJsonApiOptions _options; + private readonly IDocumentAdapter _documentAdapter; private readonly TraceLogWriter _traceWriter; - public JsonApiReader(IJsonApiDeserializer deserializer, IJsonApiRequest request, IResourceGraph resourceGraph, ILoggerFactory loggerFactory) + public JsonApiReader(IJsonApiOptions options, IDocumentAdapter documentAdapter, ILoggerFactory loggerFactory) { - ArgumentGuard.NotNull(deserializer, nameof(deserializer)); - ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(documentAdapter, nameof(documentAdapter)); ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); - _deserializer = deserializer; - _request = request; - _resourceGraph = resourceGraph; + _options = options; + _documentAdapter = documentAdapter; _traceWriter = new TraceLogWriter(loggerFactory); } - public async Task ReadAsync(InputFormatterContext context) + /// + public async Task ReadAsync(HttpRequest httpRequest) { - ArgumentGuard.NotNull(context, nameof(context)); + ArgumentGuard.NotNull(httpRequest, nameof(httpRequest)); - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Read request body"); - - string body = await GetRequestBodyAsync(context.HttpContext.Request.Body); - - string url = context.HttpContext.Request.GetEncodedUrl(); - _traceWriter.LogMessage(() => $"Received {context.HttpContext.Request.Method} request at '{url}' with body: <<{body}>>"); - - object model = null; - - if (!string.IsNullOrWhiteSpace(body)) - { - try - { - model = _deserializer.Deserialize(body); - } - catch (JsonApiSerializationException exception) - { - throw ToInvalidRequestBodyException(exception, body); - } -#pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException - catch (Exception exception) -#pragma warning restore AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException - { - throw new InvalidRequestBodyException(null, null, body, exception); - } - } - - if (_request.Kind == EndpointKind.AtomicOperations) - { - AssertHasRequestBody(model, body); - } - else if (RequiresRequestBody(context.HttpContext.Request.Method)) - { - ValidateRequestBody(model, body, context.HttpContext.Request); - } + string requestBody = await GetRequestBodyAsync(httpRequest); + object model = GetModel(requestBody); return model == null ? await InputFormatterResult.NoValueAsync() : await InputFormatterResult.SuccessAsync(model); } - private async Task GetRequestBodyAsync(Stream bodyStream) - { - using var reader = new StreamReader(bodyStream, leaveOpen: true); - return await reader.ReadToEndAsync(); - } - - private InvalidRequestBodyException ToInvalidRequestBodyException(JsonApiSerializationException exception, string body) + private async Task GetRequestBodyAsync(HttpRequest httpRequest) { - if (_request.Kind != EndpointKind.AtomicOperations) - { - return new InvalidRequestBodyException(exception.GenericMessage, exception.SpecificMessage, body, exception); - } - - var requestException = new InvalidRequestBodyException(exception.GenericMessage, exception.SpecificMessage, body, exception.InnerException); + using var reader = new StreamReader(httpRequest.Body, leaveOpen: true); + string requestBody = await reader.ReadToEndAsync(); - if (exception.AtomicOperationIndex != null) - { - foreach (ErrorObject error in requestException.Errors) - { - error.Source ??= new ErrorSource(); - error.Source.Pointer = $"/atomic:operations[{exception.AtomicOperationIndex}]"; - } - } + _traceWriter.LogMessage(() => $"Received {httpRequest.Method} request at '{httpRequest.GetEncodedUrl()}' with body: <<{requestBody}>>"); - return requestException; + return requestBody; } - private bool RequiresRequestBody(string requestMethod) + private object GetModel(string requestBody) { - if (requestMethod == HttpMethods.Post || requestMethod == HttpMethods.Patch) - { - return true; - } + AssertHasRequestBody(requestBody); - return requestMethod == HttpMethods.Delete && _request.Kind == EndpointKind.Relationship; - } - - private void ValidateRequestBody(object model, string body, HttpRequest httpRequest) - { - AssertHasRequestBody(model, body); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Read request body"); - ValidateIncomingResourceType(model, httpRequest); + Document document = DeserializeDocument(requestBody, _options.SerializerReadOptions); - if (httpRequest.Method != HttpMethods.Post || _request.Kind == EndpointKind.Relationship) + try { - ValidateRequestIncludesId(model, body); - ValidatePrimaryIdValue(model, httpRequest.Path); + return _documentAdapter.Convert(document); } - - if (_request.Kind == EndpointKind.Relationship) + catch (DeserializationException exception) { - ValidateForRelationshipType(httpRequest.Method, model, body); + throw new InvalidRequestBodyException(exception.GenericMessage, exception.SpecificMessage, requestBody, exception.SourcePointer); } } [AssertionMethod] - private static void AssertHasRequestBody(object model, string body) + private static void AssertHasRequestBody(string requestBody) { - if (model == null && string.IsNullOrWhiteSpace(body)) + if (string.IsNullOrEmpty(requestBody)) { throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { @@ -153,122 +87,23 @@ private static void AssertHasRequestBody(object model, string body) } } - private void ValidateIncomingResourceType(object model, HttpRequest httpRequest) + private Document DeserializeDocument(string requestBody, JsonSerializerOptions serializerOptions) { - Type endpointResourceType = GetResourceTypeFromEndpoint(); + ArgumentGuard.NotNull(requestBody, nameof(requestBody)); - if (endpointResourceType == null) + try { - return; - } + using IDisposable _ = + CodeTimingSessionManager.Current.Measure("JsonSerializer.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - IEnumerable bodyResourceTypes = GetResourceTypesFromRequestBody(model); - - foreach (Type bodyResourceType in bodyResourceTypes) - { - if (!endpointResourceType.IsAssignableFrom(bodyResourceType)) - { - ResourceContext resourceFromEndpoint = _resourceGraph.GetResourceContext(endpointResourceType); - ResourceContext resourceFromBody = _resourceGraph.GetResourceContext(bodyResourceType); - - throw new ResourceTypeMismatchException(new HttpMethod(httpRequest.Method), httpRequest.Path, resourceFromEndpoint, resourceFromBody); - } + return JsonSerializer.Deserialize(requestBody, serializerOptions); } - } - - private Type GetResourceTypeFromEndpoint() - { - return _request.Kind == EndpointKind.Primary ? _request.PrimaryResource.ResourceType : _request.SecondaryResource?.ResourceType; - } - - private IEnumerable GetResourceTypesFromRequestBody(object model) - { - if (model is IEnumerable resourceCollection) - { - return resourceCollection.Select(resource => resource.GetType()).Distinct(); - } - - return model == null ? Enumerable.Empty() : model.GetType().AsEnumerable(); - } - - private void ValidateRequestIncludesId(object model, string body) - { - bool hasMissingId = model is IEnumerable list ? HasMissingId(list) : HasMissingId(model); - - if (hasMissingId) - { - throw new InvalidRequestBodyException("Request body must include 'id' element.", null, body); - } - } - - private void ValidatePrimaryIdValue(object model, PathString requestPath) - { - if (_request.Kind == EndpointKind.Primary) - { - if (TryGetId(model, out string bodyId) && bodyId != _request.PrimaryId) - { - throw new ResourceIdMismatchException(bodyId, _request.PrimaryId, requestPath); - } - } - } - - /// - /// Checks if the deserialized request body has an ID included. - /// - private bool HasMissingId(object model) - { - return TryGetId(model, out string id) && id == null; - } - - /// - /// Checks if all elements in the deserialized request body have an ID included. - /// - private bool HasMissingId(IEnumerable models) - { - foreach (object model in models) - { - if (TryGetId(model, out string id) && id == null) - { - return true; - } - } - - return false; - } - - private static bool TryGetId(object model, out string id) - { - if (model is IIdentifiable identifiable) - { - id = identifiable.StringId; - return true; - } - - id = null; - return false; - } - - [AssertionMethod] - private void ValidateForRelationshipType(string requestMethod, object model, string body) - { - if (_request.Relationship is HasOneAttribute) - { - if (requestMethod == HttpMethods.Post || requestMethod == HttpMethods.Delete) - { - throw new ToManyRelationshipRequiredException(_request.Relationship.PublicName); - } - - if (model is { } and not IIdentifiable) - { - throw new InvalidRequestBodyException("Expected single data element for to-one relationship.", - $"Expected single data element for '{_request.Relationship.PublicName}' relationship.", body); - } - } - - if (_request.Relationship is HasManyAttribute && model is not IEnumerable) + catch (JsonException exception) { - throw new InvalidRequestBodyException("Expected data[] element for to-many relationship.", - $"Expected data[] element for '{_request.Relationship.PublicName}' relationship.", body); + // 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 InvalidRequestBodyException(null, exception.Message, requestBody, null, exception); } } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializationException.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializationException.cs deleted file mode 100644 index 15fd8c8075..0000000000 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializationException.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using JetBrains.Annotations; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// The error that is thrown when (de)serialization of a JSON:API body fails. - /// - [PublicAPI] - public sealed class JsonApiSerializationException : Exception - { - public string GenericMessage { get; } - public string SpecificMessage { get; } - public int? AtomicOperationIndex { get; } - - public JsonApiSerializationException(string genericMessage, string specificMessage, Exception innerException = null, int? atomicOperationIndex = null) - : base(genericMessage, innerException) - { - GenericMessage = genericMessage; - SpecificMessage = specificMessage; - AtomicOperationIndex = atomicOperationIndex; - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/JsonObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs similarity index 95% rename from src/JsonApiDotNetCore/Serialization/JsonObjectConverter.cs rename to src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs index 2a365317c4..51f19ac274 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonObjectConverter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs @@ -1,7 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.JsonConverters { public abstract class JsonObjectConverter : JsonConverter { diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs new file mode 100644 index 0000000000..63c258be28 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs @@ -0,0 +1,165 @@ +using System; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + public sealed class AtomicOperationObjectAdapter : IAtomicOperationObjectAdapter + { + private readonly IOperationResourceDataAdapter _operationResourceDataAdapter; + private readonly IAtomicReferenceAdapter _atomicReferenceAdapter; + private readonly IRelationshipDataAdapter _relationshipDataAdapter; + private readonly IResourceGraph _resourceGraph; + private readonly IJsonApiOptions _options; + + public AtomicOperationObjectAdapter(IResourceGraph resourceGraph, IJsonApiOptions options, IAtomicReferenceAdapter atomicReferenceAdapter, + IOperationResourceDataAdapter operationResourceDataAdapter, IRelationshipDataAdapter relationshipDataAdapter) + { + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(atomicReferenceAdapter, nameof(atomicReferenceAdapter)); + ArgumentGuard.NotNull(operationResourceDataAdapter, nameof(operationResourceDataAdapter)); + ArgumentGuard.NotNull(relationshipDataAdapter, nameof(relationshipDataAdapter)); + + _resourceGraph = resourceGraph; + _options = options; + _atomicReferenceAdapter = atomicReferenceAdapter; + _operationResourceDataAdapter = operationResourceDataAdapter; + _relationshipDataAdapter = relationshipDataAdapter; + } + + /// + public OperationContainer Convert(AtomicOperationObject atomicOperationObject, RequestAdapterState state) + { + AssertNoHref(atomicOperationObject, state); + + WriteOperationKind writeOperation = ConvertOperationCode(atomicOperationObject, state); + + state.WritableTargetedFields = new TargetedFields(); + + state.WritableRequest = new JsonApiRequest + { + Kind = EndpointKind.AtomicOperations, + WriteOperation = writeOperation + }; + + (ResourceIdentityRequirements requirements, IIdentifiable primaryResource) = ConvertRef(atomicOperationObject, state); + + if (writeOperation == WriteOperationKind.CreateResource || writeOperation == WriteOperationKind.UpdateResource) + { + primaryResource = _operationResourceDataAdapter.Convert(atomicOperationObject.Data, requirements, state); + } + + return new OperationContainer(writeOperation, primaryResource, state.WritableTargetedFields, state.Request); + } + + private static void AssertNoHref(AtomicOperationObject atomicOperationObject, RequestAdapterState state) + { + if (atomicOperationObject.Href != null) + { + using (state.Position.PushElement("href")) + { + throw new DeserializationException(state.Position, "Usage of the 'href' element is not supported.", null); + } + } + } + + private WriteOperationKind ConvertOperationCode(AtomicOperationObject atomicOperationObject, RequestAdapterState state) + { + switch (atomicOperationObject.Code) + { + case AtomicOperationCode.Add: + { + if (atomicOperationObject.Ref is { Relationship: null }) + { + using (state.Position.PushElement("ref")) + { + throw new DeserializationException(state.Position, "The 'ref.relationship' element is required.", null); + } + } + + return atomicOperationObject.Ref == null ? WriteOperationKind.CreateResource : WriteOperationKind.AddToRelationship; + } + case AtomicOperationCode.Update: + { + return atomicOperationObject.Ref?.Relationship != null ? WriteOperationKind.SetRelationship : WriteOperationKind.UpdateResource; + } + case AtomicOperationCode.Remove: + { + if (atomicOperationObject.Ref == null) + { + throw new DeserializationException(state.Position, "The 'ref' element is required.", null); + } + + return atomicOperationObject.Ref.Relationship != null ? WriteOperationKind.RemoveFromRelationship : WriteOperationKind.DeleteResource; + } + } + + throw new NotSupportedException($"Unknown operation code '{atomicOperationObject.Code}'."); + } + + private (ResourceIdentityRequirements requirements, IIdentifiable primaryResource) ConvertRef(AtomicOperationObject atomicOperationObject, + RequestAdapterState state) + { + ResourceIdentityRequirements requirements = CreateIdentityRequirements(state); + IIdentifiable primaryResource = null; + + AtomicReferenceResult refResult = atomicOperationObject.Ref != null + ? _atomicReferenceAdapter.Convert(atomicOperationObject.Ref, requirements, state) + : null; + + if (refResult != null) + { + requirements = new ResourceIdentityRequirements + { + ResourceContext = refResult.ResourceContext, + IdConstraint = requirements.IdConstraint, + IdValue = refResult.Resource.StringId, + LidValue = refResult.Resource.LocalId, + RelationshipName = refResult.Relationship?.PublicName + }; + + state.WritableRequest.PrimaryId = refResult.Resource.StringId; + state.WritableRequest.PrimaryResource = refResult.ResourceContext; + state.WritableRequest.Relationship = refResult.Relationship; + state.WritableRequest.IsCollection = refResult.Relationship is HasManyAttribute; + + ConvertRefRelationship(atomicOperationObject.Data, refResult, state); + + primaryResource = refResult.Resource; + } + + return (requirements, primaryResource); + } + + private ResourceIdentityRequirements CreateIdentityRequirements(RequestAdapterState state) + { + JsonElementConstraint? idConstraint = state.Request.WriteOperation == WriteOperationKind.CreateResource + ? _options.AllowClientGeneratedIds ? null : JsonElementConstraint.Forbidden + : JsonElementConstraint.Required; + + return new ResourceIdentityRequirements + { + IdConstraint = idConstraint + }; + } + + private void ConvertRefRelationship(SingleOrManyData relationshipData, AtomicReferenceResult refResult, RequestAdapterState state) + { + if (refResult.Relationship != null) + { + ResourceContext resourceContextInData = _resourceGraph.GetResourceContext(refResult.Relationship.RightType); + state.WritableRequest.SecondaryResource = resourceContextInData; + + state.WritableTargetedFields.Relationships.Add(refResult.Relationship); + + object rightValue = _relationshipDataAdapter.Convert(relationshipData, refResult.Relationship, true, state); + refResult.Relationship.SetValue(refResult.Resource, rightValue); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs new file mode 100644 index 0000000000..5f4dffdd17 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs @@ -0,0 +1,69 @@ +using System; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + public sealed class AtomicReferenceAdapter : ResourceIdentityAdapter, IAtomicReferenceAdapter + { + public AtomicReferenceAdapter(IResourceGraph resourceGraph, IResourceFactory resourceFactory) + : base(resourceGraph, resourceFactory) + { + } + + /// + public AtomicReferenceResult Convert(AtomicReference atomicReference, ResourceIdentityRequirements requirements, RequestAdapterState state) + { + ArgumentGuard.NotNull(atomicReference, nameof(atomicReference)); + ArgumentGuard.NotNull(requirements, nameof(requirements)); + ArgumentGuard.NotNull(state, nameof(state)); + + using IDisposable _ = state.Position.PushElement("ref"); + + (IIdentifiable resource, ResourceContext resourceContext) = ConvertResourceIdentity(atomicReference, requirements, state); + + RelationshipAttribute relationship = atomicReference.Relationship != null + ? ConvertRelationship(atomicReference.Relationship, resourceContext, state) + : null; + + return new AtomicReferenceResult(resource, resourceContext, relationship); + } + + private RelationshipAttribute ConvertRelationship(string relationshipName, ResourceContext resourceContext, RequestAdapterState state) + { + using IDisposable _ = state.Position.PushElement("relationship"); + + RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(relationshipName); + + AssertIsKnownRelationship(relationship, relationshipName, state); + AssertToManyInAddOrRemoveRelationship(relationship, state); + + return relationship; + } + + private static void AssertIsKnownRelationship(RelationshipAttribute relationship, string relationshipName, RequestAdapterState state) + { + if (relationship == null) + { + throw new DeserializationException(state.Position, "Request body includes unknown relationship.", + $"Relationship '{relationshipName}' does not exist."); + } + } + + private static void AssertToManyInAddOrRemoveRelationship(RelationshipAttribute relationship, RequestAdapterState state) + { + bool requireToManyRelationship = state.Request.WriteOperation == WriteOperationKind.AddToRelationship || + state.Request.WriteOperation == WriteOperationKind.RemoveFromRelationship; + + if (requireToManyRelationship && relationship is not HasManyAttribute) + { + throw new DeserializationException(state.Position, "Only to-many relationships can be targeted through this operation.", + $"Relationship '{relationship.PublicName}' must be a to-many relationship."); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceResult.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceResult.cs new file mode 100644 index 0000000000..43343054c3 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceResult.cs @@ -0,0 +1,28 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// The result of validating and converting "ref" in an entry of an atomic:operations request. + /// + [PublicAPI] + public sealed class AtomicReferenceResult + { + public IIdentifiable Resource { get; } + public ResourceContext ResourceContext { get; } + public RelationshipAttribute Relationship { get; } + + public AtomicReferenceResult(IIdentifiable resource, ResourceContext resourceContext, RelationshipAttribute relationship) + { + ArgumentGuard.NotNull(resource, nameof(resource)); + ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + + Resource = resource; + ResourceContext = resourceContext; + Relationship = relationship; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentAdapter.cs new file mode 100644 index 0000000000..4f0848546b --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentAdapter.cs @@ -0,0 +1,41 @@ +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + public sealed class DocumentAdapter : IDocumentAdapter + { + private readonly IJsonApiRequest _request; + private readonly ITargetedFields _targetedFields; + private readonly IResourceDocumentAdapter _resourceDocumentAdapter; + private readonly IOperationsDocumentAdapter _operationsDocumentAdapter; + + public DocumentAdapter(IJsonApiRequest request, ITargetedFields targetedFields, IResourceDocumentAdapter resourceDocumentAdapter, + IOperationsDocumentAdapter operationsDocumentAdapter) + { + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); + ArgumentGuard.NotNull(resourceDocumentAdapter, nameof(resourceDocumentAdapter)); + ArgumentGuard.NotNull(operationsDocumentAdapter, nameof(operationsDocumentAdapter)); + + _request = request; + _targetedFields = targetedFields; + _resourceDocumentAdapter = resourceDocumentAdapter; + _operationsDocumentAdapter = operationsDocumentAdapter; + } + + /// + public object Convert(Document document) + { + ArgumentGuard.NotNull(document, nameof(document)); + + using var context = new RequestAdapterState(_request, _targetedFields); + + return context.Request.Kind == EndpointKind.AtomicOperations + ? _operationsDocumentAdapter.Convert(document, context) + : _resourceDocumentAdapter.Convert(document, context); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicOperationObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicOperationObjectAdapter.cs new file mode 100644 index 0000000000..a1567896d2 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicOperationObjectAdapter.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Validates and converts a single operation inside an atomic:operations request. + /// + public interface IAtomicOperationObjectAdapter + { + /// + /// Validates and converts the specified . + /// + OperationContainer Convert(AtomicOperationObject atomicOperationObject, RequestAdapterState state); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicReferenceAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicReferenceAdapter.cs new file mode 100644 index 0000000000..7d47288608 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicReferenceAdapter.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Validates and converts a 'ref' element in an entry of an atomic:operations request. It appears in most kinds of operations and typically indicates + /// what would otherwise have been in the endpoint URL, if it were a resource request. + /// + public interface IAtomicReferenceAdapter + { + /// + /// Validates and converts the specified . + /// + AtomicReferenceResult Convert(AtomicReference atomicReference, ResourceIdentityRequirements requirements, RequestAdapterState state); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentAdapter.cs new file mode 100644 index 0000000000..4343e3bb49 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentAdapter.cs @@ -0,0 +1,38 @@ +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// The entry point for validating and converting the deserialized from the request body into a model. The produced models are + /// used in ASP.NET Model Binding. + /// + public interface IDocumentAdapter + { + /// + /// Validates and converts the specified . Possible return values: + /// + /// + /// + /// ]]> (operations) + /// + /// + /// + /// + /// ]]> (to-many relationship, unknown relationship) + /// + /// + /// + /// + /// (resource, to-one relationship) + /// + /// + /// + /// + /// (to-one relationship) + /// + /// + /// + /// + object Convert(Document document); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationResourceDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationResourceDataAdapter.cs new file mode 100644 index 0000000000..bc3615bad6 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationResourceDataAdapter.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Validates and converts the data from an entry in an atomic:operations request that creates or updates a resource. + /// + public interface IOperationResourceDataAdapter + { + /// + /// Validates and converts the specified . + /// + IIdentifiable Convert(SingleOrManyData data, ResourceIdentityRequirements requirements, RequestAdapterState state); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationsDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationsDocumentAdapter.cs new file mode 100644 index 0000000000..9e4f34c208 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationsDocumentAdapter.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Validates and converts a belonging to an atomic:operations request. + /// + public interface IOperationsDocumentAdapter + { + /// + /// Validates and converts the specified . + /// + IList Convert(Document document, RequestAdapterState state); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IRelationshipDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IRelationshipDataAdapter.cs new file mode 100644 index 0000000000..28f8d77989 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IRelationshipDataAdapter.cs @@ -0,0 +1,24 @@ +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Validates and converts the data from a relationship. It appears in a relationship request, in the relationships of a POST/PATCH resource request, in + /// an entry of an atomic:operations request that targets a relationship and in the relationships of an operations entry that creates or updates a + /// resource. + /// + public interface IRelationshipDataAdapter + { + /// + /// Validates and converts the specified . + /// + object Convert(SingleOrManyData data, RelationshipAttribute relationship, bool useToManyElementType, RequestAdapterState state); + + /// + /// Validates and converts the specified . + /// + object Convert(SingleOrManyData data, RelationshipAttribute relationship, bool useToManyElementType, + RequestAdapterState state); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataAdapter.cs new file mode 100644 index 0000000000..197f0d0607 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataAdapter.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Validates and converts the data from a resource in a POST/PATCH resource request. + /// + public interface IResourceDataAdapter + { + /// + /// Validates and converts the specified . + /// + IIdentifiable Convert(SingleOrManyData data, ResourceIdentityRequirements requirements, RequestAdapterState state); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDocumentAdapter.cs new file mode 100644 index 0000000000..8ac5d119b6 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDocumentAdapter.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Validates and converts a belonging to a resource or relationship request. + /// + public interface IResourceDocumentAdapter + { + /// + /// Validates and converts the specified . + /// + object Convert(Document document, RequestAdapterState state); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceIdentifierObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceIdentifierObjectAdapter.cs new file mode 100644 index 0000000000..e9e6383d40 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceIdentifierObjectAdapter.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Validates and converts a . It appears in the data object(s) of a relationship. + /// + public interface IResourceIdentifierObjectAdapter + { + /// + /// Validates and converts the specified . + /// + IIdentifiable Convert(ResourceIdentifierObject resourceIdentifierObject, ResourceIdentityRequirements requirements, RequestAdapterState state); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceObjectAdapter.cs new file mode 100644 index 0000000000..578009434c --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceObjectAdapter.cs @@ -0,0 +1,19 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Validates and converts a . It appears in a POST/PATCH resource request and an entry in an atomic:operations request that + /// creates or updates a resource. + /// + public interface IResourceObjectAdapter + { + /// + /// Validates and converts the specified . + /// + (IIdentifiable resource, ResourceContext resourceContext) Convert(ResourceObject resourceObject, ResourceIdentityRequirements requirements, + RequestAdapterState state); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/JsonElementConstraint.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/JsonElementConstraint.cs new file mode 100644 index 0000000000..d2979809db --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/JsonElementConstraint.cs @@ -0,0 +1,21 @@ +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Lists constraints for the presence or absence of a JSON element. + /// + [PublicAPI] + public enum JsonElementConstraint + { + /// + /// A value for the field is not allowed. + /// + Forbidden, + + /// + /// A value for the field is required. + /// + Required + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationResourceDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationResourceDataAdapter.cs new file mode 100644 index 0000000000..02c641dbeb --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationResourceDataAdapter.cs @@ -0,0 +1,28 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + public sealed class OperationResourceDataAdapter : ResourceDataAdapter, IOperationResourceDataAdapter + { + public OperationResourceDataAdapter(IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectAdapter resourceObjectAdapter) + : base(resourceDefinitionAccessor, resourceObjectAdapter) + { + } + + protected override (IIdentifiable resource, ResourceContext resourceContext) ConvertResourceObject(SingleOrManyData data, + ResourceIdentityRequirements requirements, RequestAdapterState state) + { + // This override ensures that we enrich IJsonApiRequest before calling into IResourceDefinition, so it is ready for consumption there. + + (IIdentifiable resource, ResourceContext resourceContext) = base.ConvertResourceObject(data, requirements, state); + + state.WritableRequest.PrimaryResource = resourceContext; + state.WritableRequest.PrimaryId = resource.StringId; + + return (resource, resourceContext); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs new file mode 100644 index 0000000000..60964f39da --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + public sealed class OperationsDocumentAdapter : IOperationsDocumentAdapter + { + private readonly IJsonApiOptions _options; + private readonly IAtomicOperationObjectAdapter _atomicOperationObjectAdapter; + + public OperationsDocumentAdapter(IJsonApiOptions options, IAtomicOperationObjectAdapter atomicOperationObjectAdapter) + { + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(atomicOperationObjectAdapter, nameof(atomicOperationObjectAdapter)); + + _options = options; + _atomicOperationObjectAdapter = atomicOperationObjectAdapter; + } + + /// + public IList Convert(Document document, RequestAdapterState state) + { + ArgumentGuard.NotNull(state, nameof(state)); + AssertHasOperations(document.Operations, state); + + using (state.Position.PushElement("atomic:operations")) + { + AssertMaxOperationsNotExceeded(document.Operations, state); + + return ConvertOperations(document.Operations, state); + } + } + + private static void AssertHasOperations(IEnumerable atomicOperationObjects, RequestAdapterState state) + { + if (atomicOperationObjects.IsNullOrEmpty()) + { + throw new DeserializationException(state.Position, "No operations found.", null); + } + } + + private void AssertMaxOperationsNotExceeded(ICollection atomicOperationObjects, RequestAdapterState state) + { + if (atomicOperationObjects.Count > _options.MaximumOperationsPerRequest) + { + throw new DeserializationException(state.Position, "Request exceeds the maximum number of operations.", + $"The number of operations in this request ({atomicOperationObjects.Count}) is higher than {_options.MaximumOperationsPerRequest}."); + } + } + + private IList ConvertOperations(IEnumerable atomicOperationObjects, RequestAdapterState state) + { + var operations = new List(); + int operationIndex = 0; + + foreach (AtomicOperationObject atomicOperationObject in atomicOperationObjects) + { + using (state.Position.PushArrayIndex(operationIndex)) + { + OperationContainer operation = _atomicOperationObjectAdapter.Convert(atomicOperationObject, state); + operations.Add(operation); + + operationIndex++; + } + } + + return operations; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs new file mode 100644 index 0000000000..21517459b6 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + public sealed class RelationshipDataAdapter : IRelationshipDataAdapter + { + private static readonly CollectionConverter CollectionConverter = new(); + + private readonly IResourceGraph _resourceGraph; + private readonly IResourceIdentifierObjectAdapter _resourceIdentifierObjectAdapter; + + public RelationshipDataAdapter(IResourceGraph resourceGraph, IResourceIdentifierObjectAdapter resourceIdentifierObjectAdapter) + { + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(resourceIdentifierObjectAdapter, nameof(resourceIdentifierObjectAdapter)); + + _resourceGraph = resourceGraph; + _resourceIdentifierObjectAdapter = resourceIdentifierObjectAdapter; + } + + /// + public object Convert(SingleOrManyData data, RelationshipAttribute relationship, bool useToManyElementType, RequestAdapterState state) + { + SingleOrManyData identifierData = ToIdentifierData(data); + return Convert(identifierData, relationship, useToManyElementType, state); + } + + private static SingleOrManyData ToIdentifierData(SingleOrManyData data) + { + if (!data.IsAssigned) + { + return default; + } + + object newValue = null; + + if (data.ManyValue != null) + { + newValue = data.ManyValue.Select(resourceObject => new ResourceIdentifierObject + { + Type = resourceObject.Type, + Id = resourceObject.Id, + Lid = resourceObject.Lid + }); + } + else if (data.SingleValue != null) + { + newValue = new ResourceIdentifierObject + { + Type = data.SingleValue.Type, + Id = data.SingleValue.Id, + Lid = data.SingleValue.Lid + }; + } + + return new SingleOrManyData(newValue); + } + + /// + public object Convert(SingleOrManyData data, RelationshipAttribute relationship, bool useToManyElementType, + RequestAdapterState state) + { + ArgumentGuard.NotNull(relationship, nameof(relationship)); + ArgumentGuard.NotNull(state, nameof(state)); + + using IDisposable _ = state.Position.PushElement("data"); + + ResourceContext rightResourceContext = _resourceGraph.GetResourceContext(relationship.RightType); + + var requirements = new ResourceIdentityRequirements + { + ResourceContext = rightResourceContext, + IdConstraint = JsonElementConstraint.Required, + RelationshipName = relationship.PublicName, + UseLegacyError = state.Request.Kind != EndpointKind.Relationship + }; + + return relationship is HasOneAttribute + ? ConvertToOneRelationshipData(data, relationship, requirements, state) + : ConvertToManyRelationshipData(data, relationship, requirements, useToManyElementType, state); + } + + private IIdentifiable ConvertToOneRelationshipData(SingleOrManyData data, RelationshipAttribute relationship, + ResourceIdentityRequirements requirements, RequestAdapterState state) + { + AssertHasNoManyValue(data, relationship, state); + + return data.SingleValue != null ? _resourceIdentifierObjectAdapter.Convert(data.SingleValue, requirements, state) : null; + } + + private static void AssertHasNoManyValue(SingleOrManyData data, RelationshipAttribute relationship, RequestAdapterState state) + { + if (data.ManyValue != null) + { + throw new DeserializationException(state.Position, "Expected single data element for to-one relationship.", + $"Expected single data element for '{relationship.PublicName}' relationship."); + } + } + + private IEnumerable ConvertToManyRelationshipData(SingleOrManyData data, RelationshipAttribute relationship, + ResourceIdentityRequirements requirements, bool useToManyElementType, RequestAdapterState state) + { + AssertHasManyValue(data, relationship, state); + + int arrayIndex = 0; + var rightResources = new List(); + + foreach (ResourceIdentifierObject resourceIdentifierObject in data.ManyValue) + { + using IDisposable _ = state.Position.PushArrayIndex(arrayIndex); + + IIdentifiable rightResource = _resourceIdentifierObjectAdapter.Convert(resourceIdentifierObject, requirements, state); + rightResources.Add(rightResource); + + arrayIndex++; + } + + if (useToManyElementType) + { + return CollectionConverter.CopyToTypedCollection(rightResources, relationship.Property.PropertyType); + } + + var resourceSet = new HashSet(IdentifiableComparer.Instance); + resourceSet.AddRange(rightResources); + return resourceSet; + } + + private static void AssertHasManyValue(SingleOrManyData data, RelationshipAttribute relationship, RequestAdapterState state) + { + if (data.ManyValue == null) + { + throw new DeserializationException(state.Position, "Expected data[] element for to-many relationship.", + $"Expected data[] element for '{relationship.PublicName}' relationship."); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterPosition.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterPosition.cs new file mode 100644 index 0000000000..992130f182 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterPosition.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Tracks the location within an object tree when validating and converting a request body. + /// + [PublicAPI] + public sealed class RequestAdapterPosition + { + private readonly Stack _stack = new(); + private readonly IDisposable _disposable; + + public RequestAdapterPosition() + { + _disposable = new PopStackOnDispose(this); + } + + public IDisposable PushElement(string name) + { + ArgumentGuard.NotNullNorEmpty(name, nameof(name)); + + _stack.Push($"/{name}"); + return _disposable; + } + + public IDisposable PushArrayIndex(int index) + { + _stack.Push($"[{index}]"); + return _disposable; + } + + public string ToSourcePointer() + { + if (!_stack.Any()) + { + return null; + } + + var builder = new StringBuilder(); + var clone = new Stack(_stack); + + while (clone.Any()) + { + string element = clone.Pop(); + builder.Append(element); + } + + return builder.ToString(); + } + + public override string ToString() + { + return ToSourcePointer() ?? string.Empty; + } + + private sealed class PopStackOnDispose : IDisposable + { + private readonly RequestAdapterPosition _owner; + + public PopStackOnDispose(RequestAdapterPosition owner) + { + _owner = owner; + } + + public void Dispose() + { + _owner._stack.Pop(); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterState.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterState.cs new file mode 100644 index 0000000000..1d9ea10f08 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterState.cs @@ -0,0 +1,70 @@ +using System; +using JetBrains.Annotations; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Tracks state while adapting objects from into the shape that controller actions accept. + /// + [PublicAPI] + public sealed class RequestAdapterState : IDisposable + { + private static readonly TargetedFields EmptyTargetedFields = new(); + private readonly IJsonApiRequest _backupRequest; + + public IJsonApiRequest InjectableRequest { get; } + public ITargetedFields InjectableTargetedFields { get; } + + public JsonApiRequest WritableRequest { get; set; } + public TargetedFields WritableTargetedFields { get; set; } + + public RequestAdapterPosition Position { get; } = new(); + public IJsonApiRequest Request => WritableRequest ?? InjectableRequest; + + public RequestAdapterState(IJsonApiRequest request, ITargetedFields targetedFields) + { + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); + + InjectableRequest = request; + InjectableTargetedFields = targetedFields; + + if (request.Kind == EndpointKind.AtomicOperations) + { + _backupRequest = new JsonApiRequest(); + _backupRequest.CopyFrom(InjectableRequest); + } + } + + public void RefreshInjectables() + { + if (WritableRequest != null) + { + InjectableRequest.CopyFrom(WritableRequest); + } + + if (WritableTargetedFields != null) + { + InjectableTargetedFields.CopyFrom(WritableTargetedFields); + } + } + + public void Dispose() + { + // For resource requests, we'd like the injected state to become the final state. + // But for operations, it makes more sense to reset than to reflect the last operation. + + if (_backupRequest != null) + { + InjectableTargetedFields.CopyFrom(EmptyTargetedFields); + InjectableRequest.CopyFrom(_backupRequest); + } + else + { + RefreshInjectables(); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataAdapter.cs new file mode 100644 index 0000000000..abb7e5bf72 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataAdapter.cs @@ -0,0 +1,65 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + public class ResourceDataAdapter : IResourceDataAdapter + { + private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; + private readonly IResourceObjectAdapter _resourceObjectAdapter; + + public ResourceDataAdapter(IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectAdapter resourceObjectAdapter) + { + ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); + ArgumentGuard.NotNull(resourceObjectAdapter, nameof(resourceObjectAdapter)); + + _resourceDefinitionAccessor = resourceDefinitionAccessor; + _resourceObjectAdapter = resourceObjectAdapter; + } + + /// + public IIdentifiable Convert(SingleOrManyData data, ResourceIdentityRequirements requirements, RequestAdapterState state) + { + ArgumentGuard.NotNull(requirements, nameof(requirements)); + ArgumentGuard.NotNull(state, nameof(state)); + AssertHasData(data, state); + + using (state.Position.PushElement("data")) + { + AssertNoManyValue(data, state); + + (IIdentifiable resource, ResourceContext _) = ConvertResourceObject(data, requirements, state); + + // Ensure that IResourceDefinition extensibility point sees the current operation, it case it injects IJsonApiRequest. + state.RefreshInjectables(); + + _resourceDefinitionAccessor.OnDeserialize(resource); + return resource; + } + } + + private static void AssertHasData(SingleOrManyData data, RequestAdapterState state) + { + if (data.Value == null) + { + throw new DeserializationException(state.Position, "The 'data' element is required.", null); + } + } + + private static void AssertNoManyValue(SingleOrManyData data, RequestAdapterState state) + { + if (data.ManyValue != null) + { + throw new DeserializationException(state.Position, "Expected 'data' object instead of array.", null); + } + } + + protected virtual (IIdentifiable resource, ResourceContext resourceContext) ConvertResourceObject(SingleOrManyData data, + ResourceIdentityRequirements requirements, RequestAdapterState state) + { + return _resourceObjectAdapter.Convert(data.SingleValue, requirements, state); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDocumentAdapter.cs new file mode 100644 index 0000000000..724b1c0834 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDocumentAdapter.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Net; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + public sealed class ResourceDocumentAdapter : IResourceDocumentAdapter + { + private readonly IJsonApiOptions _options; + private readonly IResourceDataAdapter _resourceDataAdapter; + private readonly IRelationshipDataAdapter _relationshipDataAdapter; + + public ResourceDocumentAdapter(IJsonApiOptions options, IResourceDataAdapter resourceDataAdapter, IRelationshipDataAdapter relationshipDataAdapter) + { + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(resourceDataAdapter, nameof(resourceDataAdapter)); + ArgumentGuard.NotNull(relationshipDataAdapter, nameof(relationshipDataAdapter)); + + _options = options; + _resourceDataAdapter = resourceDataAdapter; + _relationshipDataAdapter = relationshipDataAdapter; + } + + /// + public object Convert(Document document, RequestAdapterState state) + { + state.WritableTargetedFields = new TargetedFields(); + + switch (state.Request.WriteOperation) + { + case WriteOperationKind.CreateResource: + case WriteOperationKind.UpdateResource: + { + ResourceIdentityRequirements requirements = CreateIdentityRequirements(state); + return _resourceDataAdapter.Convert(document.Data, requirements, state); + } + case WriteOperationKind.SetRelationship: + case WriteOperationKind.AddToRelationship: + case WriteOperationKind.RemoveFromRelationship: + { + if (state.Request.Relationship == null) + { + // Let the controller throw for unknown relationship, because it knows the relationship name that was used. + return new HashSet(IdentifiableComparer.Instance); + } + + AssertToManyInAddOrRemoveRelationship(state); + + state.WritableTargetedFields.Relationships.Add(state.Request.Relationship); + return _relationshipDataAdapter.Convert(document.Data, state.Request.Relationship, false, state); + } + } + + return null; + } + + private ResourceIdentityRequirements CreateIdentityRequirements(RequestAdapterState state) + { + JsonElementConstraint? idConstraint = state.Request.WriteOperation == WriteOperationKind.CreateResource + ? _options.AllowClientGeneratedIds ? null : JsonElementConstraint.Forbidden + : JsonElementConstraint.Required; + + var requirements = new ResourceIdentityRequirements + { + ResourceContext = state.Request.PrimaryResource, + IdConstraint = idConstraint, + IdValue = state.Request.PrimaryId + }; + + return requirements; + } + + private static void AssertToManyInAddOrRemoveRelationship(RequestAdapterState state) + { + bool requireToManyRelationship = state.Request.WriteOperation == WriteOperationKind.AddToRelationship || + state.Request.WriteOperation == WriteOperationKind.RemoveFromRelationship; + + if (requireToManyRelationship && state.Request.Relationship is not HasManyAttribute) + { + throw new JsonApiException(new ErrorObject(HttpStatusCode.Forbidden) + { + Title = "Only to-many relationships can be targeted through this endpoint.", + Detail = $"Relationship '{state.Request.Relationship.PublicName}' must be a to-many relationship." + }); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentifierObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentifierObjectAdapter.cs new file mode 100644 index 0000000000..acea2c1a74 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentifierObjectAdapter.cs @@ -0,0 +1,26 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + public sealed class ResourceIdentifierObjectAdapter : ResourceIdentityAdapter, IResourceIdentifierObjectAdapter + { + public ResourceIdentifierObjectAdapter(IResourceGraph resourceGraph, IResourceFactory resourceFactory) + : base(resourceGraph, resourceFactory) + { + } + + /// + public IIdentifiable Convert(ResourceIdentifierObject resourceIdentifierObject, ResourceIdentityRequirements requirements, RequestAdapterState state) + { + ArgumentGuard.NotNull(resourceIdentifierObject, nameof(resourceIdentifierObject)); + ArgumentGuard.NotNull(requirements, nameof(requirements)); + ArgumentGuard.NotNull(state, nameof(state)); + + (IIdentifiable resource, ResourceContext _) = ConvertResourceIdentity(resourceIdentifierObject, requirements, state); + return resource; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs new file mode 100644 index 0000000000..24819d1b71 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs @@ -0,0 +1,216 @@ +using System; +using System.Net; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Base class for validating and converting objects that represent an identity. + /// + public abstract class ResourceIdentityAdapter + { + private readonly IResourceGraph _resourceGraph; + private readonly IResourceFactory _resourceFactory; + + protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory resourceFactory) + { + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); + + _resourceGraph = resourceGraph; + _resourceFactory = resourceFactory; + } + + protected (IIdentifiable resource, ResourceContext resourceContext) ConvertResourceIdentity(IResourceIdentity identity, + ResourceIdentityRequirements requirements, RequestAdapterState state) + { + ArgumentGuard.NotNull(identity, nameof(identity)); + ArgumentGuard.NotNull(requirements, nameof(requirements)); + ArgumentGuard.NotNull(state, nameof(state)); + + ResourceContext resourceContext = ConvertType(identity, requirements, state); + + bool hasNone = identity.Id == null && identity.Lid == null; + bool hasBoth = identity.Id != null && identity.Lid != null; + + if (requirements.IdConstraint == JsonElementConstraint.Required ? hasNone || hasBoth : hasBoth) + { + string parent = identity is AtomicReference + ? "'ref' element" + : + requirements.RelationshipName != null && + state.Request.WriteOperation is WriteOperationKind.CreateResource or WriteOperationKind.UpdateResource + ? + $"'{requirements.RelationshipName}' relationship" + : "'data' element"; + + throw new DeserializationException(state.Position, + state.Request.Kind == EndpointKind.AtomicOperations + ? "Request body must include 'id' or 'lid' element." + : "Request body must include 'id' element.", + state.Request.Kind == EndpointKind.AtomicOperations + ? $"Expected 'id' or 'lid' element in {parent}." + : $"Expected 'id' element in {parent}."); + } + + if (requirements.IdConstraint == JsonElementConstraint.Forbidden && identity.Id != null) + { + using (state.Position.PushElement("id")) + { + throw new ResourceIdInCreateResourceNotAllowedException(state.Request.Kind == EndpointKind.AtomicOperations, + state.Position.ToSourcePointer()); + } + } + + if (requirements.IdValue != null && identity.Id != null && identity.Id != requirements.IdValue) + { + using (state.Position.PushElement("id")) + { + if (state.Request.Kind == EndpointKind.AtomicOperations) + { + throw new DeserializationException(state.Position, "Resource ID mismatch between 'ref.id' and 'data.id' element.", + $"Expected resource with ID '{requirements.IdValue}' in 'data.id', instead of '{identity.Id}'."); + } + + throw new JsonApiException(new ErrorObject(HttpStatusCode.Conflict) + { + Title = "Resource ID mismatch between request body and endpoint URL.", + Detail = $"Expected resource ID '{requirements.IdValue}', instead of '{identity.Id}'.", + Source = new ErrorSource + { + Pointer = state.Position.ToSourcePointer() + } + }); + } + } + + if (requirements.LidValue != null && identity.Lid != null && identity.Lid != requirements.LidValue) + { + using (state.Position.PushElement("lid")) + { + throw new DeserializationException(state.Position, "Resource local ID mismatch between 'ref.lid' and 'data.lid' element.", + $"Expected resource with local ID '{requirements.LidValue}' in 'data.lid', instead of '{identity.Lid}'."); + } + } + + if (requirements.IdValue != null && identity.Lid != null) + { + using (state.Position.PushElement("lid")) + { + throw new DeserializationException(state.Position, "Resource identity mismatch between 'ref.id' and 'data.lid' element.", + $"Expected resource with ID '{requirements.IdValue}' in 'data.id', instead of '{identity.Lid}' in 'data.lid'."); + } + } + + if (requirements.LidValue != null && identity.Id != null) + { + using (state.Position.PushElement("id")) + { + throw new DeserializationException(state.Position, "Resource identity mismatch between 'ref.lid' and 'data.id' element.", + $"Expected resource with local ID '{requirements.LidValue}' in 'data.lid', instead of '{identity.Id}' in 'data.id'."); + } + } + + IIdentifiable resource = _resourceFactory.CreateInstance(resourceContext.ResourceType); + AssignStringId(identity, resource, state); + resource.LocalId = ConvertLid(identity, state); + + return (resource, resourceContext); + } + + private ResourceContext ConvertType(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) + { + if (identity.Type == null) + { + string parent = identity is AtomicReference + ? "'ref' element" + : + requirements?.RelationshipName != null && + state.Request.WriteOperation is WriteOperationKind.CreateResource or WriteOperationKind.UpdateResource + ? + $"'{requirements.RelationshipName}' relationship" + : "'data' element"; + + throw new DeserializationException(state.Position, "Request body must include 'type' element.", $"Expected 'type' element in {parent}."); + } + + using IDisposable _ = state.Position.PushElement("type"); + + ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(identity.Type); + + if (resourceContext == null) + { + throw new DeserializationException(state.Position, "Request body includes unknown resource type.", + $"Resource type '{identity.Type}' does not exist."); + } + + if (requirements?.ResourceContext != null && !requirements.ResourceContext.ResourceType.IsAssignableFrom(resourceContext.ResourceType)) + { + if (requirements.UseLegacyError) + { + throw new DeserializationException(state.Position, "Relationship contains incompatible resource type.", + $"Relationship '{requirements.RelationshipName}' contains incompatible resource type '{resourceContext.PublicName}'."); + } + + if (state.Request.Kind == EndpointKind.AtomicOperations) + { + throw new DeserializationException(state.Position, "Resource type mismatch between 'ref.type' and 'data.type' element.", + $"Expected resource of type '{requirements.ResourceContext.PublicName}' in 'data.type', instead of '{resourceContext.PublicName}'."); + } + + string title = requirements.RelationshipName != null ? "Resource type is incompatible with relationship type." : + state.Request.Kind == EndpointKind.AtomicOperations ? "Resource type is incompatible with type in ref." : + "Resource type is incompatible with endpoint URL."; + + string detail = requirements.RelationshipName != null + ? $"Type '{resourceContext.PublicName}' is incompatible with type '{requirements.ResourceContext.PublicName}' of relationship '{requirements.RelationshipName}'." + : $"Type '{resourceContext.PublicName}' is incompatible with type '{requirements.ResourceContext.PublicName}'."; + + throw new JsonApiException(new ErrorObject(HttpStatusCode.Conflict) + { + Title = title, + Detail = detail, + Source = new ErrorSource + { + Pointer = state.Position.ToSourcePointer() + } + }); + } + + return resourceContext; + } + + private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state) + { + if (identity.Id != null) + { + using IDisposable _ = state.Position.PushElement("id"); + + try + { + resource.StringId = identity.Id; + } + catch (FormatException exception) + { + throw new DeserializationException(state.Position, null, exception.Message); + } + } + } + + private string ConvertLid(IResourceIdentity identity, RequestAdapterState state) + { + using IDisposable _ = state.Position.PushElement("lid"); + + if (state.Request.Kind != EndpointKind.AtomicOperations && identity.Lid != null) + { + throw new DeserializationException(state.Position, null, "Local IDs cannot be used at this endpoint."); + } + + return identity.Lid; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityRequirements.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityRequirements.cs new file mode 100644 index 0000000000..2dcdfb6a23 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityRequirements.cs @@ -0,0 +1,43 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Defines requirements to validate an instance against. + /// + [PublicAPI] + public sealed class ResourceIdentityRequirements + { + /// + /// When not null, indicates that the "type" element must be compatible with the specified resource type. + /// + public ResourceContext ResourceContext { get; init; } + + /// + /// When not null, indicates the presence or absence of the "id" element. + /// + public JsonElementConstraint? IdConstraint { get; init; } + + /// + /// When not null, indicates what the value of the "id" element must be. + /// + public string IdValue { get; init; } + + /// + /// When not null, indicates what the value of the "lid" element must be. + /// + public string LidValue { get; init; } + + /// + /// When not null, indicates the name of the relationship to use in error messages. + /// + public string RelationshipName { get; init; } + + /// + /// This temporary property will be removed in a future commit. + /// + public bool UseLegacyError { get; init; } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs new file mode 100644 index 0000000000..ba304e2b4e --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + public sealed class ResourceObjectAdapter : ResourceIdentityAdapter, IResourceObjectAdapter + { + private readonly IJsonApiOptions _options; + private readonly IRelationshipDataAdapter _relationshipDataAdapter; + + public ResourceObjectAdapter(IResourceGraph resourceGraph, IResourceFactory resourceFactory, IJsonApiOptions options, + IRelationshipDataAdapter relationshipDataAdapter) + : base(resourceGraph, resourceFactory) + { + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(relationshipDataAdapter, nameof(relationshipDataAdapter)); + + _options = options; + _relationshipDataAdapter = relationshipDataAdapter; + } + + /// + public (IIdentifiable resource, ResourceContext resourceContext) Convert(ResourceObject resourceObject, ResourceIdentityRequirements requirements, + RequestAdapterState state) + { + ArgumentGuard.NotNull(resourceObject, nameof(resourceObject)); + ArgumentGuard.NotNull(requirements, nameof(requirements)); + ArgumentGuard.NotNull(state, nameof(state)); + + (IIdentifiable resource, ResourceContext resourceContext) = ConvertResourceIdentity(resourceObject, requirements, state); + + ConvertAttributes(resourceObject.Attributes, resource, resourceContext, state); + ConvertRelationships(resourceObject.Relationships, resource, resourceContext, state); + + return (resource, resourceContext); + } + + private void ConvertAttributes(IDictionary resourceObjectAttributes, IIdentifiable resource, ResourceContext resourceContext, + RequestAdapterState state) + { + using IDisposable _ = state.Position.PushElement("attributes"); + + foreach ((string attributeName, object attributeValue) in resourceObjectAttributes.EmptyIfNull()) + { + ConvertAttribute(resource, attributeName, attributeValue, resourceContext, state); + } + } + + private void ConvertAttribute(IIdentifiable resource, string attributeName, object attributeValue, ResourceContext resourceContext, + RequestAdapterState state) + { + using IDisposable _ = state.Position.PushElement(attributeName); + + AttrAttribute attr = resourceContext.TryGetAttributeByPublicName(attributeName); + + if (!AssertIsKnownAttribute(attr, attributeName, state)) + { + return; + } + + AssertNoInvalidAttribute(attributeValue, state); + AssertNoBlockedCreate(attr, state); + AssertNoBlockedChange(attr, state); + AssertNotReadOnly(attr, state); + + attr.SetValue(resource, attributeValue); + state.WritableTargetedFields.Attributes.Add(attr); + } + + [AssertionMethod] + private bool AssertIsKnownAttribute(AttrAttribute attr, string attributeName, RequestAdapterState state) + { + if (attr == null) + { + if (_options.AllowUnknownFieldsInRequestBody) + { + return false; + } + + throw new DeserializationException(state.Position, "Request body includes unknown attribute.", $"Attribute '{attributeName}' does not exist."); + } + + return true; + } + + private static void AssertNoInvalidAttribute(object attributeValue, RequestAdapterState state) + { + if (attributeValue is JsonInvalidAttributeInfo info) + { + if (info == JsonInvalidAttributeInfo.Id) + { + throw new DeserializationException(state.Position, null, "Resource ID is read-only."); + } + + string typeName = info.AttributeType.GetFriendlyTypeName(); + + throw new DeserializationException(state.Position, null, + $"Failed to convert attribute '{info.AttributeName}' with value '{info.JsonValue}' of type '{info.JsonType}' to type '{typeName}'."); + } + } + + private static void AssertNoBlockedCreate(AttrAttribute attr, RequestAdapterState state) + { + if (state.Request.WriteOperation == WriteOperationKind.CreateResource && !attr.Capabilities.HasFlag(AttrCapabilities.AllowCreate)) + { + throw new DeserializationException(state.Position, "Setting the initial value of the requested attribute is not allowed.", + $"Setting the initial value of '{attr.PublicName}' is not allowed."); + } + } + + private static void AssertNoBlockedChange(AttrAttribute attr, RequestAdapterState state) + { + if (state.Request.WriteOperation == WriteOperationKind.UpdateResource && !attr.Capabilities.HasFlag(AttrCapabilities.AllowChange)) + { + throw new DeserializationException(state.Position, "Changing the value of the requested attribute is not allowed.", + $"Changing the value of '{attr.PublicName}' is not allowed."); + } + } + + private static void AssertNotReadOnly(AttrAttribute attr, RequestAdapterState state) + { + if (attr.Property.SetMethod == null) + { + throw new DeserializationException(state.Position, "Attribute is read-only.", $"Attribute '{attr.PublicName}' is read-only."); + } + } + + private void ConvertRelationships(IDictionary resourceObjectRelationships, IIdentifiable resource, + ResourceContext resourceContext, RequestAdapterState state) + { + using IDisposable _ = state.Position.PushElement("relationships"); + + foreach ((string relationshipName, RelationshipObject relationshipObject) in resourceObjectRelationships.EmptyIfNull()) + { + ConvertRelationship(relationshipName, relationshipObject.Data, resource, resourceContext, state); + } + } + + private void ConvertRelationship(string relationshipName, SingleOrManyData relationshipData, IIdentifiable resource, + ResourceContext resourceContext, RequestAdapterState state) + { + using IDisposable _ = state.Position.PushElement(relationshipName); + + RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(relationshipName); + + if (!AssertIsKnownRelationship(relationship, relationshipName, state)) + { + return; + } + + object rightValue = _relationshipDataAdapter.Convert(relationshipData, relationship, true, state); + + relationship.SetValue(resource, rightValue); + state.WritableTargetedFields.Relationships.Add(relationship); + } + + [AssertionMethod] + private bool AssertIsKnownRelationship(RelationshipAttribute relationship, string relationshipName, RequestAdapterState state) + { + if (relationship == null) + { + if (_options.AllowUnknownFieldsInRequestBody) + { + return false; + } + + throw new DeserializationException(state.Position, "Request body includes unknown relationship.", + $"Relationship '{relationshipName}' does not exist."); + } + + return true; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs deleted file mode 100644 index 592f389dcb..0000000000 --- a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs +++ /dev/null @@ -1,523 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using Humanizer; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Resources.Internal; -using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.AspNetCore.Http; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// Server deserializer implementation of the . - /// - [PublicAPI] - public class RequestDeserializer : BaseDeserializer, IJsonApiDeserializer - { - private readonly ITargetedFields _targetedFields; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IJsonApiRequest _request; - private readonly IJsonApiOptions _options; - private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; - - public RequestDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory, ITargetedFields targetedFields, - IHttpContextAccessor httpContextAccessor, IJsonApiRequest request, IJsonApiOptions options, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(resourceGraph, resourceFactory, options) - { - ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); - ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); - ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); - - _targetedFields = targetedFields; - _httpContextAccessor = httpContextAccessor; - _request = request; - _options = options; - _resourceDefinitionAccessor = resourceDefinitionAccessor; - } - - /// - public object Deserialize(string body) - { - ArgumentGuard.NotNullNorEmpty(body, nameof(body)); - - if (_request.Kind == EndpointKind.Relationship) - { - _targetedFields.Relationships.Add(_request.Relationship); - } - - if (_request.Kind == EndpointKind.AtomicOperations) - { - return DeserializeOperationsDocument(body); - } - - object instance = DeserializeData(body, _options.SerializerReadOptions); - - if (instance is IIdentifiable resource && _request.Kind != EndpointKind.Relationship) - { - _resourceDefinitionAccessor.OnDeserialize(resource); - } - - AssertResourceIdIsNotTargeted(_targetedFields); - - return instance; - } - - private object DeserializeOperationsDocument(string body) - { - Document document = DeserializeDocument(body, _options.SerializerReadOptions); - - if ((document?.Operations).IsNullOrEmpty()) - { - throw new JsonApiSerializationException("No operations found.", null); - } - - if (document.Operations.Count > _options.MaximumOperationsPerRequest) - { - throw new JsonApiSerializationException("Request exceeds the maximum number of operations.", - $"The number of operations in this request ({document.Operations.Count}) is higher than {_options.MaximumOperationsPerRequest}."); - } - - var operations = new List(); - AtomicOperationIndex = 0; - - foreach (AtomicOperationObject operation in document.Operations) - { - OperationContainer container = DeserializeOperation(operation); - operations.Add(container); - - AtomicOperationIndex++; - } - - return operations; - } - - private OperationContainer DeserializeOperation(AtomicOperationObject operation) - { - _targetedFields.Attributes.Clear(); - _targetedFields.Relationships.Clear(); - - AssertHasNoHref(operation); - - WriteOperationKind writeOperation = GetWriteOperationKind(operation); - - switch (writeOperation) - { - case WriteOperationKind.CreateResource: - case WriteOperationKind.UpdateResource: - { - return ParseForCreateOrUpdateResourceOperation(operation, writeOperation); - } - case WriteOperationKind.DeleteResource: - { - return ParseForDeleteResourceOperation(operation, writeOperation); - } - } - - bool requireToManyRelationship = - writeOperation == WriteOperationKind.AddToRelationship || writeOperation == WriteOperationKind.RemoveFromRelationship; - - return ParseForRelationshipOperation(operation, writeOperation, requireToManyRelationship); - } - - [AssertionMethod] - private void AssertHasNoHref(AtomicOperationObject operation) - { - if (operation.Href != null) - { - throw new JsonApiSerializationException("Usage of the 'href' element is not supported.", null, atomicOperationIndex: AtomicOperationIndex); - } - } - - private WriteOperationKind GetWriteOperationKind(AtomicOperationObject operation) - { - switch (operation.Code) - { - case AtomicOperationCode.Add: - { - if (operation.Ref is { Relationship: null }) - { - throw new JsonApiSerializationException("The 'ref.relationship' element is required.", null, - atomicOperationIndex: AtomicOperationIndex); - } - - return operation.Ref == null ? WriteOperationKind.CreateResource : WriteOperationKind.AddToRelationship; - } - case AtomicOperationCode.Update: - { - return operation.Ref?.Relationship != null ? WriteOperationKind.SetRelationship : WriteOperationKind.UpdateResource; - } - case AtomicOperationCode.Remove: - { - if (operation.Ref == null) - { - throw new JsonApiSerializationException("The 'ref' element is required.", null, atomicOperationIndex: AtomicOperationIndex); - } - - return operation.Ref.Relationship != null ? WriteOperationKind.RemoveFromRelationship : WriteOperationKind.DeleteResource; - } - } - - throw new NotSupportedException($"Unknown operation code '{operation.Code}'."); - } - - private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperationObject operation, WriteOperationKind writeOperation) - { - ResourceObject resourceObject = GetRequiredSingleDataForResourceOperation(operation); - - AssertElementHasType(resourceObject, "data"); - AssertElementHasIdOrLid(resourceObject, "data", writeOperation != WriteOperationKind.CreateResource); - - ResourceContext primaryResourceContext = GetExistingResourceContext(resourceObject.Type); - - AssertCompatibleId(resourceObject, primaryResourceContext.IdentityType); - - if (operation.Ref != null) - { - // For resource update, 'ref' is optional. But when specified, it must match with 'data'. - - AssertElementHasType(operation.Ref, "ref"); - AssertElementHasIdOrLid(operation.Ref, "ref", true); - - ResourceContext resourceContextInRef = GetExistingResourceContext(operation.Ref.Type); - - if (!primaryResourceContext.Equals(resourceContextInRef)) - { - throw new JsonApiSerializationException("Resource type mismatch between 'ref.type' and 'data.type' element.", - $"Expected resource of type '{resourceContextInRef.PublicName}' in 'data.type', instead of '{primaryResourceContext.PublicName}'.", - atomicOperationIndex: AtomicOperationIndex); - } - - AssertSameIdentityInRefData(operation, resourceObject); - } - - var request = new JsonApiRequest - { - Kind = EndpointKind.AtomicOperations, - PrimaryResource = primaryResourceContext, - WriteOperation = writeOperation - }; - - _request.CopyFrom(request); - - IIdentifiable primaryResource = ParseResourceObject(operation.Data.SingleValue); - - _resourceDefinitionAccessor.OnDeserialize(primaryResource); - - request.PrimaryId = primaryResource.StringId; - _request.CopyFrom(request); - - var targetedFields = new TargetedFields - { - Attributes = _targetedFields.Attributes.ToHashSet(), - Relationships = _targetedFields.Relationships.ToHashSet() - }; - - AssertResourceIdIsNotTargeted(targetedFields); - - return new OperationContainer(writeOperation, primaryResource, targetedFields, request); - } - - private ResourceObject GetRequiredSingleDataForResourceOperation(AtomicOperationObject operation) - { - if (operation.Data.Value == null) - { - throw new JsonApiSerializationException("The 'data' element is required.", null, atomicOperationIndex: AtomicOperationIndex); - } - - if (operation.Data.SingleValue == null) - { - throw new JsonApiSerializationException("Expected single data element for create/update resource operation.", null, - atomicOperationIndex: AtomicOperationIndex); - } - - return operation.Data.SingleValue; - } - - [AssertionMethod] - private void AssertElementHasType(IResourceIdentity resourceIdentity, string elementPath) - { - if (resourceIdentity.Type == null) - { - throw new JsonApiSerializationException($"The '{elementPath}.type' element is required.", null, atomicOperationIndex: AtomicOperationIndex); - } - } - - private void AssertElementHasIdOrLid(IResourceIdentity resourceIdentity, string elementPath, bool isRequired) - { - bool hasNone = resourceIdentity.Id == null && resourceIdentity.Lid == null; - bool hasBoth = resourceIdentity.Id != null && resourceIdentity.Lid != null; - - if (isRequired ? hasNone || hasBoth : hasBoth) - { - throw new JsonApiSerializationException($"The '{elementPath}.id' or '{elementPath}.lid' element is required.", null, - atomicOperationIndex: AtomicOperationIndex); - } - } - - private void AssertCompatibleId(IResourceIdentity resourceIdentity, Type idType) - { - if (resourceIdentity.Id != null) - { - try - { - RuntimeTypeConverter.ConvertType(resourceIdentity.Id, idType); - } - catch (FormatException exception) - { - throw new JsonApiSerializationException(null, exception.Message, null, AtomicOperationIndex); - } - } - } - - private void AssertSameIdentityInRefData(AtomicOperationObject operation, IResourceIdentity resourceIdentity) - { - 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 '{resourceIdentity.Id}'.", - atomicOperationIndex: AtomicOperationIndex); - } - - 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 '{resourceIdentity.Lid}'.", - atomicOperationIndex: AtomicOperationIndex); - } - - 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 '{resourceIdentity.Lid}' in 'data.lid'.", - atomicOperationIndex: AtomicOperationIndex); - } - - 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 '{resourceIdentity.Id}' in 'data.id'.", - atomicOperationIndex: AtomicOperationIndex); - } - } - - private OperationContainer ParseForDeleteResourceOperation(AtomicOperationObject operation, WriteOperationKind writeOperation) - { - AssertElementHasType(operation.Ref, "ref"); - AssertElementHasIdOrLid(operation.Ref, "ref", true); - - ResourceContext primaryResourceContext = GetExistingResourceContext(operation.Ref.Type); - - AssertCompatibleId(operation.Ref, primaryResourceContext.IdentityType); - - IIdentifiable primaryResource = ResourceFactory.CreateInstance(primaryResourceContext.ResourceType); - primaryResource.StringId = operation.Ref.Id; - primaryResource.LocalId = operation.Ref.Lid; - - var request = new JsonApiRequest - { - Kind = EndpointKind.AtomicOperations, - PrimaryId = primaryResource.StringId, - PrimaryResource = primaryResourceContext, - WriteOperation = writeOperation - }; - - return new OperationContainer(writeOperation, primaryResource, new TargetedFields(), request); - } - - private OperationContainer ParseForRelationshipOperation(AtomicOperationObject operation, WriteOperationKind writeOperation, bool requireToMany) - { - AssertElementHasType(operation.Ref, "ref"); - AssertElementHasIdOrLid(operation.Ref, "ref", true); - - ResourceContext primaryResourceContext = GetExistingResourceContext(operation.Ref.Type); - - AssertCompatibleId(operation.Ref, primaryResourceContext.IdentityType); - - IIdentifiable primaryResource = ResourceFactory.CreateInstance(primaryResourceContext.ResourceType); - primaryResource.StringId = operation.Ref.Id; - primaryResource.LocalId = operation.Ref.Lid; - - RelationshipAttribute relationship = GetExistingRelationship(operation.Ref, primaryResourceContext); - - if (requireToMany && relationship is HasOneAttribute) - { - throw new JsonApiSerializationException($"Only to-many relationships can be targeted in '{operation.Code.ToString().Camelize()}' operations.", - $"Relationship '{operation.Ref.Relationship}' must be a to-many relationship.", atomicOperationIndex: AtomicOperationIndex); - } - - ResourceContext secondaryResourceContext = ResourceGraph.GetResourceContext(relationship.RightType); - - var request = new JsonApiRequest - { - Kind = EndpointKind.AtomicOperations, - PrimaryId = primaryResource.StringId, - PrimaryResource = primaryResourceContext, - SecondaryResource = secondaryResourceContext, - Relationship = relationship, - IsCollection = relationship is HasManyAttribute, - WriteOperation = writeOperation - }; - - _request.CopyFrom(request); - - _targetedFields.Relationships.Add(relationship); - - ParseDataForRelationship(relationship, secondaryResourceContext, operation, primaryResource); - - var targetedFields = new TargetedFields - { - Attributes = _targetedFields.Attributes.ToHashSet(), - Relationships = _targetedFields.Relationships.ToHashSet() - }; - - return new OperationContainer(writeOperation, primaryResource, targetedFields, request); - } - - private RelationshipAttribute GetExistingRelationship(AtomicReference reference, ResourceContext resourceContext) - { - RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(reference.Relationship); - - if (relationship == null) - { - throw new JsonApiSerializationException("The referenced relationship does not exist.", - $"Resource of type '{reference.Type}' does not contain a relationship named '{reference.Relationship}'.", - atomicOperationIndex: AtomicOperationIndex); - } - - return relationship; - } - - private void ParseDataForRelationship(RelationshipAttribute relationship, ResourceContext secondaryResourceContext, AtomicOperationObject operation, - IIdentifiable primaryResource) - { - if (relationship is HasOneAttribute) - { - 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.Data.SingleValue != null) - { - ValidateSingleDataForRelationship(operation.Data.SingleValue, secondaryResourceContext, "data"); - - IIdentifiable secondaryResource = ParseResourceObject(operation.Data.SingleValue); - relationship.SetValue(primaryResource, secondaryResource); - } - } - else if (relationship is HasManyAttribute) - { - if (operation.Data.ManyValue == null) - { - throw new JsonApiSerializationException("Expected data[] element for to-many relationship.", - $"Expected data[] element for '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); - } - - var secondaryResources = new List(); - - foreach (ResourceObject resourceObject in operation.Data.ManyValue) - { - ValidateSingleDataForRelationship(resourceObject, secondaryResourceContext, "data[]"); - - IIdentifiable secondaryResource = ParseResourceObject(resourceObject); - secondaryResources.Add(secondaryResource); - } - - IEnumerable rightResources = CollectionConverter.CopyToTypedCollection(secondaryResources, relationship.Property.PropertyType); - relationship.SetValue(primaryResource, rightResources); - } - } - - private void ValidateSingleDataForRelationship(ResourceObject dataResourceObject, ResourceContext resourceContext, string elementPath) - { - AssertElementHasType(dataResourceObject, elementPath); - AssertElementHasIdOrLid(dataResourceObject, elementPath, true); - - ResourceContext resourceContextInData = GetExistingResourceContext(dataResourceObject.Type); - - AssertCompatibleType(resourceContextInData, resourceContext, elementPath); - AssertCompatibleId(dataResourceObject, resourceContextInData.IdentityType); - } - - private void AssertCompatibleType(ResourceContext resourceContextInData, ResourceContext resourceContextInRef, string elementPath) - { - if (!resourceContextInData.ResourceType.IsAssignableFrom(resourceContextInRef.ResourceType)) - { - throw new JsonApiSerializationException($"Resource type mismatch between 'ref.relationship' and '{elementPath}.type' element.", - $"Expected resource of type '{resourceContextInRef.PublicName}' in '{elementPath}.type', instead of '{resourceContextInData.PublicName}'.", - atomicOperationIndex: AtomicOperationIndex); - } - } - - private void AssertResourceIdIsNotTargeted(ITargetedFields targetedFields) - { - if (!_request.IsReadOnly && targetedFields.Attributes.Any(attribute => attribute.Property.Name == nameof(Identifiable.Id))) - { - throw new JsonApiSerializationException("Resource ID is read-only.", null, atomicOperationIndex: AtomicOperationIndex); - } - } - - /// - /// Additional processing required for server deserialization. Flags a processed attribute or relationship as updated using - /// . - /// - /// - /// 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, RelationshipObject data = null) - { - bool isCreatingResource = IsCreatingResource(); - bool isUpdatingResource = IsUpdatingResource(); - - if (field is AttrAttribute attr) - { - if (isCreatingResource && !attr.Capabilities.HasFlag(AttrCapabilities.AllowCreate)) - { - throw new JsonApiSerializationException("Setting the initial value of the requested attribute is not allowed.", - $"Setting the initial value of '{attr.PublicName}' is not allowed.", atomicOperationIndex: AtomicOperationIndex); - } - - if (isUpdatingResource && !attr.Capabilities.HasFlag(AttrCapabilities.AllowChange)) - { - throw new JsonApiSerializationException("Changing the value of the requested attribute is not allowed.", - $"Changing the value of '{attr.PublicName}' is not allowed.", atomicOperationIndex: AtomicOperationIndex); - } - - _targetedFields.Attributes.Add(attr); - } - else if (field is RelationshipAttribute relationship) - { - _targetedFields.Relationships.Add(relationship); - } - } - - private bool IsCreatingResource() - { - return _request.Kind == EndpointKind.AtomicOperations - ? _request.WriteOperation == WriteOperationKind.CreateResource - : _request.Kind == EndpointKind.Primary && _httpContextAccessor.HttpContext!.Request.Method == HttpMethod.Post.Method; - } - - private bool IsUpdatingResource() - { - return _request.Kind == EndpointKind.AtomicOperations - ? _request.WriteOperation == WriteOperationKind.UpdateResource - : _request.Kind == EndpointKind.Primary && _httpContextAccessor.HttpContext!.Request.Method == HttpMethod.Patch.Method; - } - } -} diff --git a/src/JsonApiDotNetCore/TypeExtensions.cs b/src/JsonApiDotNetCore/TypeExtensions.cs index 15713ac5fa..50514f3128 100644 --- a/src/JsonApiDotNetCore/TypeExtensions.cs +++ b/src/JsonApiDotNetCore/TypeExtensions.cs @@ -39,7 +39,7 @@ public static string GetFriendlyTypeName(this Type type) string genericArguments = type.GetGenericArguments().Select(GetFriendlyTypeName) .Aggregate((firstType, secondType) => $"{firstType}, {secondType}"); - return $"{type.Name[..type.Name.IndexOf("`", StringComparison.Ordinal)]}" + $"<{genericArguments}>"; + return $"{type.Name[..type.Name.IndexOf("`", StringComparison.Ordinal)]}<{genericArguments}>"; } return type.Name; diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index 2136fcb55d..a10e3e6723 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -256,6 +256,7 @@ public async Task Cannot_create_resource_with_unknown_attribute() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown attribute."); error.Detail.Should().Be("Attribute 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -357,6 +358,7 @@ public async Task Cannot_create_resource_with_unknown_relationship() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship."); error.Detail.Should().Be("Relationship 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -492,7 +494,7 @@ public async Task Cannot_create_resource_for_href_element() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } [Fact] @@ -528,7 +530,7 @@ public async Task Cannot_create_resource_for_ref_element() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -596,9 +598,9 @@ public async Task Cannot_create_resource_for_missing_type() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -634,11 +636,11 @@ public async Task Cannot_create_resource_for_unknown_type() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); } [Fact] - public async Task Cannot_create_resource_for_array() + public async Task Cannot_create_resource_for_data_array() { // Arrange string newArtistName = _fakers.Performer.Generate().ArtistName; @@ -677,9 +679,9 @@ public async Task Cannot_create_resource_for_array() 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.Title.Should().Be("Failed to deserialize request body: Expected 'data' object instead of array."); error.Detail.Should().BeNull(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -719,7 +721,7 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/createdAt"); } [Fact] @@ -762,7 +764,7 @@ public async Task Cannot_create_resource_with_readonly_attribute() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/isArchived"); } [Fact] @@ -802,7 +804,7 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); 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]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/bornAt"); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs index eb3780a140..6c7f0518c2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs @@ -227,7 +227,7 @@ public async Task Cannot_create_resource_for_incompatible_ID() 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); } [Fact] @@ -263,9 +263,9 @@ public async Task Cannot_create_resource_for_ID_and_local_ID() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index d35107a8b8..dc20d85608 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -234,7 +234,7 @@ public async Task Cannot_create_for_missing_relationship_type() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); } [Fact] @@ -284,7 +284,7 @@ public async Task Cannot_create_for_unknown_relationship_type() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); } [Fact] @@ -333,7 +333,7 @@ public async Task Cannot_create_for_missing_relationship_ID() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); } [Fact] @@ -453,7 +453,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); } [Fact] @@ -572,7 +572,7 @@ public async Task Cannot_create_with_null_data_in_OneToMany_relationship() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data"); } [Fact] @@ -615,7 +615,7 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/tracks/data"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index 9153bb404d..bce49199af 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -299,7 +299,7 @@ public async Task Cannot_create_for_missing_relationship_type() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); } [Fact] @@ -346,7 +346,7 @@ public async Task Cannot_create_for_unknown_relationship_type() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); } [Fact] @@ -392,7 +392,7 @@ public async Task Cannot_create_for_missing_relationship_ID() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); } [Fact] @@ -488,7 +488,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); } [Fact] @@ -616,7 +616,7 @@ public async Task Cannot_create_with_data_array_in_relationship() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index ff0f20a15b..daba49839d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -358,7 +358,7 @@ public async Task Cannot_delete_resource_for_href_element() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } [Fact] @@ -424,9 +424,9 @@ public async Task Cannot_delete_resource_for_missing_type() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -463,7 +463,7 @@ public async Task Cannot_delete_resource_for_unknown_type() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); } [Fact] @@ -497,9 +497,9 @@ public async Task Cannot_delete_resource_for_missing_ID() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -577,7 +577,7 @@ public async Task Cannot_delete_resource_for_incompatible_ID() 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); } [Fact] @@ -613,9 +613,9 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs index 8b2a56a092..de2b72ba47 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.EntityFrameworkCore; using TestBuildingBlocks; using Xunit; @@ -41,15 +39,6 @@ public async Task Cannot_process_for_missing_request_body() error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - List performersInDatabase = await dbContext.Performers.ToListAsync(); - performersInDatabase.Should().BeEmpty(); - - List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); - tracksInDatabase.Should().BeEmpty(); - }); } [Fact] @@ -75,6 +64,35 @@ public async Task Cannot_process_for_broken_JSON_request_body() error.Source.Should().BeNull(); } + [Fact] + public async Task Cannot_process_for_missing_operations_array() + { + // Arrange + const string route = "/operations"; + + var requestBody = new + { + meta = new + { + key = "value" + } + }; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: No operations found."); + error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); + } + [Fact] public async Task Cannot_process_empty_operations_array() { @@ -99,15 +117,6 @@ public async Task Cannot_process_empty_operations_array() error.Title.Should().Be("Failed to deserialize request body: No operations found."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - List performersInDatabase = await dbContext.Performers.ToListAsync(); - performersInDatabase.Should().BeEmpty(); - - List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); - tracksInDatabase.Should().BeEmpty(); - }); } [Fact] @@ -147,15 +156,6 @@ public async Task Cannot_process_for_unknown_operation_code() error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("The JSON value could not be converted to "); error.Source.Should().BeNull(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - List performersInDatabase = await dbContext.Performers.ToListAsync(); - performersInDatabase.Should().BeEmpty(); - - List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); - tracksInDatabase.Should().BeEmpty(); - }); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs index d72a79f9b8..ae7fc2f24f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs @@ -74,7 +74,7 @@ public async Task Cannot_process_more_operations_than_maximum() 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.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations"); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index ada44f47e7..f2329b2518 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -70,8 +70,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this operation."); error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); } [Fact] @@ -264,7 +265,7 @@ public async Task Cannot_add_for_href_element() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } [Fact] @@ -299,9 +300,9 @@ public async Task Cannot_add_for_missing_type_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -339,7 +340,7 @@ public async Task Cannot_add_for_unknown_type_in_ref() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); } [Fact] @@ -374,9 +375,9 @@ public async Task Cannot_add_for_missing_ID_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -469,9 +470,9 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -508,7 +509,7 @@ public async Task Cannot_add_for_missing_relationship_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -544,9 +545,9 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() 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 '{Unknown.Relationship}'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); } [Fact] @@ -593,7 +594,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -636,9 +637,9 @@ public async Task Cannot_add_for_missing_type_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } [Fact] @@ -684,7 +685,7 @@ public async Task Cannot_add_for_unknown_type_in_data() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); } [Fact] @@ -727,9 +728,9 @@ public async Task Cannot_add_for_missing_ID_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } [Fact] @@ -774,9 +775,9 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } [Fact] @@ -899,9 +900,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index ffaff765c3..9efba9ac1f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -71,8 +71,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this operation."); error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); } [Fact] @@ -264,7 +265,7 @@ public async Task Cannot_remove_for_href_element() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } [Fact] @@ -299,9 +300,9 @@ public async Task Cannot_remove_for_missing_type_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -339,7 +340,7 @@ public async Task Cannot_remove_for_unknown_type_in_ref() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); } [Fact] @@ -374,9 +375,9 @@ public async Task Cannot_remove_for_missing_ID_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -469,9 +470,9 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -507,9 +508,9 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() 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 '{Unknown.Relationship}'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); } [Fact] @@ -556,7 +557,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -599,9 +600,9 @@ public async Task Cannot_remove_for_missing_type_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } [Fact] @@ -647,7 +648,7 @@ public async Task Cannot_remove_for_unknown_type_in_data() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); } [Fact] @@ -690,9 +691,9 @@ public async Task Cannot_remove_for_missing_ID_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } [Fact] @@ -737,9 +738,9 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } [Fact] @@ -862,9 +863,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index c1426db8a9..fe0f8887d3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -301,7 +301,7 @@ public async Task Cannot_replace_for_href_element() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } [Fact] @@ -336,9 +336,9 @@ public async Task Cannot_replace_for_missing_type_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -376,7 +376,7 @@ public async Task Cannot_replace_for_unknown_type_in_ref() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); } [Fact] @@ -411,9 +411,9 @@ public async Task Cannot_replace_for_missing_ID_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -525,7 +525,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); } [Fact] @@ -562,9 +562,9 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -600,9 +600,9 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() 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 '{Unknown.Relationship}'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); } [Fact] @@ -649,7 +649,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -692,9 +692,9 @@ public async Task Cannot_replace_for_missing_type_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } [Fact] @@ -740,7 +740,7 @@ public async Task Cannot_replace_for_unknown_type_in_data() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); } [Fact] @@ -783,9 +783,9 @@ public async Task Cannot_replace_for_missing_ID_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } [Fact] @@ -830,9 +830,9 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } [Fact] @@ -957,7 +957,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/id"); } [Fact] @@ -1009,9 +1009,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 85c153a7f1..3db15cc855 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -548,7 +548,7 @@ public async Task Cannot_create_for_href_element() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } [Fact] @@ -583,9 +583,9 @@ public async Task Cannot_create_for_missing_type_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -623,7 +623,7 @@ public async Task Cannot_create_for_unknown_type_in_ref() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); } [Fact] @@ -658,9 +658,9 @@ public async Task Cannot_create_for_missing_ID_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -764,7 +764,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); } [Fact] @@ -801,9 +801,9 @@ public async Task Cannot_create_for_ID_and_local_ID_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -839,13 +839,13 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() 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 '{Unknown.Relationship}'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); } [Fact] - public async Task Cannot_create_for_array_in_data() + public async Task Cannot_create_for_data_array() { // Arrange MusicTrack existingTrack = _fakers.MusicTrack.Generate(); @@ -895,7 +895,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -935,9 +935,9 @@ public async Task Cannot_create_for_missing_type_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -980,7 +980,7 @@ public async Task Cannot_create_for_unknown_type_in_data() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); } [Fact] @@ -1020,9 +1020,9 @@ public async Task Cannot_create_for_missing_ID_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -1064,9 +1064,9 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -1170,7 +1170,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); } [Fact] @@ -1219,9 +1219,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 3ee9307112..2aca9858df 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -340,7 +340,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data"); } [Fact] @@ -390,7 +390,7 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/tracks/data[0]"); } [Fact] @@ -441,7 +441,7 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); } [Fact] @@ -491,7 +491,7 @@ public async Task Cannot_replace_for_missing_ID_in_relationship_data() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); } [Fact] @@ -543,7 +543,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); } [Fact] @@ -678,7 +678,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 729413c8b1..73268f5c0a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -209,6 +209,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown attribute."); error.Detail.Should().Be("Attribute 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -321,6 +322,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship."); error.Detail.Should().Be("Relationship 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -631,7 +633,7 @@ public async Task Cannot_update_resource_for_href_element() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } [Fact] @@ -733,9 +735,9 @@ public async Task Cannot_update_resource_for_missing_type_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -780,9 +782,9 @@ public async Task Cannot_update_resource_for_missing_ID_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -829,9 +831,9 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_ref() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'ref' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } [Fact] @@ -903,9 +905,9 @@ public async Task Cannot_update_resource_for_missing_type_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + error.Detail.Should().Be("Expected 'type' element in 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -945,9 +947,9 @@ public async Task Cannot_update_resource_for_missing_ID_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -989,13 +991,13 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() 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(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + 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 'data' element."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] - public async Task Cannot_update_resource_for_array_in_data() + public async Task Cannot_update_resource_for_data_array() { // Arrange Performer existingPerformer = _fakers.Performer.Generate(); @@ -1041,9 +1043,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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.Title.Should().Be("Failed to deserialize request body: Expected 'data' object instead of array."); error.Detail.Should().BeNull(); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -1091,7 +1093,7 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); } [Fact] @@ -1142,7 +1144,7 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() 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 '{performerId1}' in 'data.id', instead of '{performerId2}'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); } [Fact] @@ -1190,7 +1192,7 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/lid"); } [Fact] @@ -1240,7 +1242,7 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da 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 '{performerId}' in 'data.id', instead of 'local-1' in 'data.lid'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/lid"); } [Fact] @@ -1290,7 +1292,7 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da 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 '{performerId}' in 'data.id'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); } [Fact] @@ -1333,7 +1335,7 @@ public async Task Cannot_update_resource_for_unknown_type() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); } [Fact] @@ -1425,7 +1427,7 @@ public async Task Cannot_update_resource_for_incompatible_ID() 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); } [Fact] @@ -1474,7 +1476,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/createdAt"); } [Fact] @@ -1523,7 +1525,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/isArchived"); } [Fact] @@ -1572,7 +1574,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); 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]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/id"); } [Fact] @@ -1621,7 +1623,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); 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]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/bornAt"); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index 9487a69551..5074681b56 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -564,7 +564,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_create_for_array_in_relationship_data() + public async Task Cannot_create_for_relationship_data_array() { // Arrange MusicTrack existingTrack = _fakers.MusicTrack.Generate(); @@ -619,7 +619,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); } [Fact] @@ -666,7 +666,7 @@ public async Task Cannot_create_for_missing_type_in_relationship_data() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/track/data"); } [Fact] @@ -714,7 +714,7 @@ public async Task Cannot_create_for_unknown_type_in_relationship_data() 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 '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); } [Fact] @@ -761,7 +761,7 @@ public async Task Cannot_create_for_missing_ID_in_relationship_data() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); } [Fact] @@ -810,7 +810,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); } [Fact] @@ -924,7 +924,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/Car.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/Car.cs index e50286f5a0..74ebc3a865 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/Car.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/Car.cs @@ -12,18 +12,22 @@ public sealed class Car : Identifiable [NotMapped] public override string Id { - get => $"{RegionId}:{LicensePlate}"; + get => RegionId == default && LicensePlate == default ? null : $"{RegionId}:{LicensePlate}"; set { + if (value == null) + { + RegionId = default; + LicensePlate = default; + return; + } + string[] elements = value.Split(':'); - if (elements.Length == 2) + if (elements.Length == 2 && int.TryParse(elements[0], out int regionId)) { - if (int.TryParse(elements[0], out int regionId)) - { - RegionId = regionId; - LicensePlate = elements[1]; - } + RegionId = regionId; + LicensePlate = elements[1]; } else { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiable.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiable.cs index 99a89e6b8c..ab86709eba 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiable.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiable.cs @@ -8,12 +8,12 @@ public abstract class ObfuscatedIdentifiable : Identifiable protected override string GetStringId(int value) { - return Codec.Encode(value); + return value == default ? null : Codec.Encode(value); } protected override int GetTypedId(string value) { - return Codec.Decode(value); + return value == null ? default : Codec.Decode(value); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/NonJsonApiController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/NonJsonApiController.cs index fa783d5095..3f108548ac 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/NonJsonApiController.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NonJsonApiControllers/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/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 2835828069..d1fd2fbfad 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -286,6 +286,7 @@ public async Task Cannot_create_resource_with_unknown_attribute() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown attribute."); error.Detail.Should().Be("Attribute 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/data/attributes/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -372,6 +373,7 @@ public async Task Cannot_create_resource_with_unknown_relationship() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship."); error.Detail.Should().Be("Relationship 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/data/relationships/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -454,7 +456,7 @@ public async Task Cannot_create_resource_with_client_generated_ID() 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.Title.Should().Be("Specifying the resource ID in POST resource requests is not allowed."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/id"); } @@ -479,6 +481,72 @@ public async Task Cannot_create_resource_for_missing_request_body() error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); + } + + [Fact] + public async Task Cannot_create_resource_for_missing_data() + { + // Arrange + var requestBody = new + { + meta = new + { + key = "value" + } + }; + + const string route = "/workItems"; + + // 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: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Cannot_create_resource_for_data_array() + { + // Arrange + var requestBody = new + { + data = new[] + { + new + { + type = "workItems" + } + } + }; + + const string route = "/workItems"; + + // 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: Expected 'data' object instead of array."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -509,6 +577,7 @@ public async Task Cannot_create_resource_for_missing_type() 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 'data' element."); + error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -542,6 +611,7 @@ public async Task Cannot_create_resource_for_unknown_type() 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 '{Unknown.ResourceType}' does not exist."); + error.Source.Pointer.Should().Be("/data/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -597,8 +667,9 @@ public async Task Cannot_create_on_resource_type_mismatch_between_url_and_body() 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'."); + error.Title.Should().Be("Resource type is incompatible with endpoint URL."); + error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'workItems'."); + error.Source.Pointer.Should().Be("/data/type"); } [Fact] @@ -631,6 +702,7 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() 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 'isImportant' is not allowed."); + error.Source.Pointer.Should().Be("/data/attributes/isImportant"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -665,6 +737,7 @@ public async Task Cannot_create_resource_with_readonly_attribute() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isDeprecated' is read-only."); + error.Source.Pointer.Should().Be("/data/attributes/isDeprecated"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -689,6 +762,7 @@ public async Task Cannot_create_resource_for_broken_JSON_request_body() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("'{' is invalid after a property name."); + error.Source.Should().BeNull(); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -723,6 +797,7 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Failed to convert attribute 'dueAt' with value 'not-a-valid-time' of type 'String' to type 'Nullable'."); + error.Source.Pointer.Should().Be("/data/attributes/dueAt"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs index e8dc30f79f..f43aa5e2bf 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs @@ -248,6 +248,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); + error.Source.Should().BeNull(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index b261199dc3..bc8d55a99f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -359,6 +359,7 @@ public async Task Cannot_create_for_missing_relationship_type() 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 'subscribers' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -403,6 +404,7 @@ public async Task Cannot_create_for_unknown_relationship_type() 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 '{Unknown.ResourceType}' does not exist."); + error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -446,6 +448,7 @@ public async Task Cannot_create_for_missing_relationship_ID() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().Be("Expected 'id' element in 'subscribers' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -545,6 +548,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() 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 'subscribers' contains incompatible resource type 'rgbColors'."); + error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -648,6 +652,7 @@ public async Task Cannot_create_with_null_data_in_OneToMany_relationship() 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 'subscribers' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/subscribers/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -685,6 +690,7 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() 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 'tags' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/tags/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -732,6 +738,7 @@ public async Task Cannot_create_resource_with_local_ID() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Local IDs cannot be used at this endpoint."); + error.Source.Pointer.Should().Be("/data/lid"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index 427d2d065c..61ec7f3f99 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -319,6 +319,7 @@ public async Task Cannot_create_for_missing_relationship_type() 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 'assignee' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -360,6 +361,7 @@ public async Task Cannot_create_for_unknown_relationship_type() 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 '{Unknown.ResourceType}' does not exist."); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -400,6 +402,7 @@ public async Task Cannot_create_for_missing_relationship_ID() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().Be("Expected 'id' element in 'assignee' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -443,6 +446,7 @@ public async Task Cannot_create_with_unknown_relationship_ID() 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 '{userAccountId}' in relationship 'assignee' does not exist."); + error.Source.Should().BeNull(); } [Fact] @@ -482,6 +486,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() 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 'assignee' contains incompatible resource type 'rgbColors'."); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -605,6 +610,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'assignee' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -649,6 +655,7 @@ public async Task Cannot_create_resource_with_local_ID() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Local IDs cannot be used at this endpoint."); + error.Source.Pointer.Should().Be("/data/lid"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs index e9d7116889..b80327f7ad 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs @@ -75,6 +75,7 @@ public async Task Cannot_delete_unknown_resource() 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 '{workItemId}' does not exist."); + error.Source.Should().BeNull(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index 3ea7bad7c6..61d516b395 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -58,8 +58,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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.Title.Should().Be("Only to-many relationships can be targeted through this endpoint."); error.Detail.Should().Be("Relationship 'assignee' must be a to-many relationship."); + error.Source.Should().BeNull(); } [Fact] @@ -204,6 +205,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); } [Fact] @@ -243,6 +245,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'data' element."); + error.Source.Pointer.Should().Be("/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -285,6 +288,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist."); + error.Source.Pointer.Should().Be("/data[0]/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -325,7 +329,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().BeNull(); + error.Detail.Should().Be("Expected 'id' element in 'data' element."); + error.Source.Pointer.Should().Be("/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -512,6 +517,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{workItemId}' does not exist."); + error.Source.Should().BeNull(); } [Fact] @@ -552,6 +558,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.Relationship}'."); + error.Source.Should().BeNull(); } [Fact] @@ -591,10 +598,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'workTags' in POST request body at endpoint " + - $"'/workItems/{existingWorkItem.StringId}/relationships/tags', instead of 'userAccounts'."); + error.Title.Should().Be("Resource type is incompatible with relationship type."); + error.Detail.Should().Be("Type 'userAccounts' is incompatible with type 'workTags' of relationship 'tags'."); + error.Source.Pointer.Should().Be("/data[0]/type"); } [Fact] @@ -712,6 +718,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'subscribers' relationship."); + error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -747,6 +754,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'tags' relationship."); + error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index 177be7be30..a1f5e9d0ed 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -73,8 +73,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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.Title.Should().Be("Only to-many relationships can be targeted through this endpoint."); error.Detail.Should().Be("Relationship 'assignee' must be a to-many relationship."); + error.Source.Should().BeNull(); } [Fact] @@ -360,6 +361,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'data' element."); + error.Source.Pointer.Should().Be("/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -402,6 +404,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist."); + error.Source.Pointer.Should().Be("/data[0]/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -442,7 +445,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().BeNull(); + error.Detail.Should().Be("Expected 'id' element in 'data' element."); + error.Source.Pointer.Should().Be("/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -629,6 +633,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{workItemId}' does not exist."); + error.Source.Should().BeNull(); } [Fact] @@ -669,6 +674,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.Relationship}'."); + error.Source.Should().BeNull(); } [Fact] @@ -708,10 +714,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'workTags' in DELETE request body at endpoint " + - $"'/workItems/{existingWorkItem.StringId}/relationships/tags', instead of 'userAccounts'."); + error.Title.Should().Be("Resource type is incompatible with relationship type."); + error.Detail.Should().Be("Type 'userAccounts' is incompatible with type 'workTags' of relationship 'tags'."); + error.Source.Pointer.Should().Be("/data[0]/type"); } [Fact] @@ -831,6 +836,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'subscribers' relationship."); + error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -866,6 +872,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'tags' relationship."); + error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index 10c1743f6a..c559bdcea2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -236,6 +236,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); } [Fact] @@ -275,6 +276,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'data' element."); + error.Source.Pointer.Should().Be("/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -317,6 +319,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist."); + error.Source.Pointer.Should().Be("/data[0]/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -357,7 +360,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().BeNull(); + error.Detail.Should().Be("Expected 'id' element in 'data' element."); + error.Source.Pointer.Should().Be("/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -408,11 +412,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{userAccountId1}' in relationship 'subscribers' does not exist."); + error1.Source.Should().BeNull(); 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 '{userAccountId2}' in relationship 'subscribers' does not exist."); + error2.Source.Should().BeNull(); } [Fact] @@ -461,11 +467,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{tagId1}' in relationship 'tags' does not exist."); + error1.Source.Should().BeNull(); 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 '{tagId2}' in relationship 'tags' does not exist."); + error2.Source.Should().BeNull(); } [Fact] @@ -537,6 +545,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{workItemId}' does not exist."); + error.Source.Should().BeNull(); } [Fact] @@ -577,6 +586,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.Relationship}'."); + error.Source.Should().BeNull(); } [Fact] @@ -616,10 +626,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'workTags' in PATCH request body at endpoint " + - $"'/workItems/{existingWorkItem.StringId}/relationships/tags', instead of 'userAccounts'."); + error.Title.Should().Be("Resource type is incompatible with relationship type."); + error.Detail.Should().Be("Type 'userAccounts' is incompatible with type 'workTags' of relationship 'tags'."); + error.Source.Pointer.Should().Be("/data[0]/type"); } [Fact] @@ -704,6 +713,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'subscribers' relationship."); + error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -739,6 +749,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'tags' relationship."); + error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index 95c34e3c62..865e51ef1d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -269,6 +269,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); } [Fact] @@ -305,6 +306,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'data' element."); + error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -344,6 +346,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist."); + error.Source.Pointer.Should().Be("/data/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -381,7 +384,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().BeNull(); + error.Detail.Should().Be("Expected 'id' element in 'data' element."); + error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -423,6 +427,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{userAccountId}' in relationship 'assignee' does not exist."); + error.Source.Should().BeNull(); } [Fact] @@ -495,6 +500,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{workItemId}' does not exist."); + error.Source.Should().BeNull(); } [Fact] @@ -532,6 +538,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.Relationship}'."); + error.Source.Should().BeNull(); } [Fact] @@ -568,10 +575,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'userAccounts' in PATCH request body at endpoint " + - $"'/workItems/{existingWorkItem.StringId}/relationships/assignee', instead of 'rgbColors'."); + error.Title.Should().Be("Resource type is incompatible with relationship type."); + error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'assignee'."); + error.Source.Pointer.Should().Be("/data/type"); } [Fact] @@ -613,6 +619,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'assignee' relationship."); + error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index ffa47a0900..327899f2d1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -443,6 +443,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'subscribers' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -496,6 +497,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist."); + error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -548,6 +550,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().Be("Expected 'id' element in 'subscribers' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -694,6 +697,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'subscribers' contains incompatible resource type 'rgbColors'."); + error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -802,6 +806,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'subscribers' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/subscribers/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -848,6 +853,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'tags' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/tags/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 3903e1e3e6..bf7f409b99 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -127,6 +127,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown attribute."); error.Detail.Should().Be("Attribute 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/data/attributes/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -226,6 +227,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship."); error.Detail.Should().Be("Relationship 'doesNotExist' does not exist."); + error.Source.Pointer.Should().Be("/data/relationships/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -658,6 +660,89 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); + } + + [Fact] + public async Task Cannot_update_resource_for_missing_data() + { + // Arrange + WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.WorkItems.Add(existingWorkItem); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + meta = new + { + key = "value" + } + }; + + 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: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Cannot_create_resource_for_data_array() + { + // Arrange + WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.WorkItems.Add(existingWorkItem); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new[] + { + new + { + type = "workItems", + id = existingWorkItem.StringId + } + } + }; + + 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: Expected 'data' object instead of array."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -694,6 +779,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'data' element."); + error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -733,6 +819,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist."); + error.Source.Pointer.Should().Be("/data/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -770,7 +857,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().BeNull(); + error.Detail.Should().Be("Expected 'id' element in 'data' element."); + error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -836,6 +924,7 @@ public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() 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 '{workItemId}' does not exist."); + error.Source.Should().BeNull(); } [Fact] @@ -871,10 +960,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 PATCH request body at endpoint " + - $"'/workItems/{existingWorkItem.StringId}', instead of 'rgbColors'."); + error.Title.Should().Be("Resource type is incompatible with endpoint URL."); + error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'workItems'."); + error.Source.Pointer.Should().Be("/data/type"); } [Fact] @@ -911,9 +999,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource ID mismatch between request body and endpoint URL."); - - error.Detail.Should().Be($"Expected resource ID '{existingWorkItems[1].StringId}' in PATCH request body at endpoint " + - $"'/workItems/{existingWorkItems[1].StringId}', instead of '{existingWorkItems[0].StringId}'."); + error.Detail.Should().Be($"Expected resource ID '{existingWorkItems[1].StringId}', instead of '{existingWorkItems[0].StringId}'."); + error.Source.Pointer.Should().Be("/data/id"); } [Fact] @@ -955,6 +1042,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'isImportant' is not allowed."); + error.Source.Pointer.Should().Be("/data/attributes/isImportant"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -998,6 +1086,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isDeprecated' is read-only."); + error.Source.Pointer.Should().Be("/data/attributes/isDeprecated"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -1030,6 +1119,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("Expected end of string, but instead reached end of data."); + error.Source.Should().BeNull(); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -1073,6 +1163,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Resource ID is read-only."); + error.Source.Pointer.Should().Be("/data/attributes/id"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -1115,6 +1206,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert ID '{existingWorkItem.Id}' of type 'Number' to type 'String'."); + error.Source.Should().BeNull(); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -1162,6 +1254,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Match("Failed to convert attribute 'dueAt' with value '*start*end*' of type 'Object' to type 'Nullable'."); + error.Source.Pointer.Should().Be("/data/attributes/dueAt"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index 70365d1746..669e75a46a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -481,6 +481,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'assignee' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -531,6 +532,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{Unknown.ResourceType}' does not exist."); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -580,6 +582,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().Be("Expected 'id' element in 'assignee' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -632,6 +635,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 '{userAccountId}' in relationship 'assignee' does not exist."); + error.Source.Should().BeNull(); } [Fact] @@ -680,6 +684,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'assignee' contains incompatible resource type 'rgbColors'."); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -734,6 +739,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'assignee' relationship."); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs new file mode 100644 index 0000000000..ef0c0af9d1 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Design; +using FluentAssertions; +using FluentAssertions.Extensions; +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.JsonConverters; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.Serialization.RequestAdapters; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace JsonApiDotNetCoreTests.UnitTests.Serialization +{ + public sealed class InputConversionTests + { + [Fact] + public void Converts_various_data_types_with_values() + { + // Arrange + DocumentAdapter documentAdapter = CreateDocumentAdapter(resourceGraph => new JsonApiRequest + { + Kind = EndpointKind.Primary, + PrimaryResource = resourceGraph.GetResourceContext(), + WriteOperation = WriteOperationKind.CreateResource + }); + + const bool booleanValue = true; + const bool nullableBooleanValue = false; + const char charValue = 'A'; + const char nullableCharValue = '?'; + const ulong unsignedLongValue = ulong.MaxValue; + const ulong nullableUnsignedLongValue = 9_000_000_000UL; + const decimal decimalValue = 19.95m; + const decimal nullableDecimalValue = 12.50m; + const float floatValue = (float)1 / 3; + const float nullableFloatValue = (float)3 / 5; + const string stringValue = "text"; + var guidValue = Guid.NewGuid(); + var nullableGuidValue = Guid.NewGuid(); + DateTime dateTimeValue = 12.July(1982); + DateTime nullableDateTimeValue = 18.October(2028); + DateTimeOffset dateTimeOffsetValue = 3.March(1999).WithOffset(7.Hours()); + DateTimeOffset nullableDateTimeOffsetValue = 28.February(2009).WithOffset(-2.Hours()); + TimeSpan timeSpanValue = 4.Hours().And(58.Minutes()); + TimeSpan nullableTimeSpanValue = 35.Seconds().And(44.Milliseconds()); + const DayOfWeek enumValue = DayOfWeek.Wednesday; + const DayOfWeek nullableEnumValue = DayOfWeek.Sunday; + + var complexObject = new ComplexObject + { + Value = "Single" + }; + + var complexObjectList = new List + { + new() + { + Value = "One" + }, + new() + { + Value = "Two" + } + }; + + var document = new Document + { + Data = new SingleOrManyData(new ResourceObject + { + Type = "resourceWithVariousDataTypes", + Attributes = new Dictionary + { + ["boolean"] = booleanValue, + ["nullableBoolean"] = nullableBooleanValue, + ["char"] = charValue, + ["nullableChar"] = nullableCharValue, + ["unsignedLong"] = unsignedLongValue, + ["nullableUnsignedLong"] = nullableUnsignedLongValue, + ["decimal"] = decimalValue, + ["nullableDecimal"] = nullableDecimalValue, + ["float"] = floatValue, + ["nullableFloat"] = nullableFloatValue, + ["string"] = stringValue, + ["guid"] = guidValue, + ["nullableGuid"] = nullableGuidValue, + ["dateTime"] = dateTimeValue, + ["nullableDateTime"] = nullableDateTimeValue, + ["dateTimeOffset"] = dateTimeOffsetValue, + ["nullableDateTimeOffset"] = nullableDateTimeOffsetValue, + ["timeSpan"] = timeSpanValue, + ["nullableTimeSpan"] = nullableTimeSpanValue, + ["enum"] = enumValue, + ["nullableEnum"] = nullableEnumValue, + ["complexObject"] = complexObject, + ["complexObjectList"] = complexObjectList + } + }) + }; + + // Act + var model = (ResourceWithVariousDataTypes)documentAdapter.Convert(document); + + // Assert + model.Should().NotBeNull(); + + model.Boolean.Should().Be(booleanValue); + model.NullableBoolean.Should().Be(nullableBooleanValue); + model.Char.Should().Be(charValue); + model.NullableChar.Should().Be(nullableCharValue); + model.UnsignedLong.Should().Be(unsignedLongValue); + model.NullableUnsignedLong.Should().Be(nullableUnsignedLongValue); + model.Decimal.Should().Be(decimalValue); + model.NullableDecimal.Should().Be(nullableDecimalValue); + model.Float.Should().Be(floatValue); + model.NullableFloat.Should().Be(nullableFloatValue); + model.String.Should().Be(stringValue); + model.Guid.Should().Be(guidValue); + model.NullableGuid.Should().Be(nullableGuidValue); + model.DateTime.Should().Be(dateTimeValue); + model.NullableDateTime.Should().Be(nullableDateTimeValue); + model.DateTimeOffset.Should().Be(dateTimeOffsetValue); + model.NullableDateTimeOffset.Should().Be(nullableDateTimeOffsetValue); + model.TimeSpan.Should().Be(timeSpanValue); + model.NullableTimeSpan.Should().Be(nullableTimeSpanValue); + model.Enum.Should().Be(enumValue); + model.NullableEnum.Should().Be(nullableEnumValue); + + model.ComplexObject.Should().NotBeNull(); + model.ComplexObject.Value.Should().Be(complexObject.Value); + + model.ComplexObjectList.Should().HaveCount(2); + model.ComplexObjectList[0].Value.Should().Be(complexObjectList[0].Value); + model.ComplexObjectList[1].Value.Should().Be(complexObjectList[1].Value); + } + + [Fact] + public void Converts_various_data_types_with_defaults() + { + // Arrange + DocumentAdapter documentAdapter = CreateDocumentAdapter(resourceGraph => new JsonApiRequest + { + Kind = EndpointKind.Primary, + PrimaryResource = resourceGraph.GetResourceContext(), + WriteOperation = WriteOperationKind.CreateResource + }); + + const bool booleanValue = default; + const bool nullableBooleanValue = default; + const char charValue = default; + const char nullableCharValue = default; + const ulong unsignedLongValue = default; + const ulong nullableUnsignedLongValue = default; + const decimal decimalValue = default; + const decimal nullableDecimalValue = default; + const float floatValue = default; + const float nullableFloatValue = default; + const string stringValue = default; + Guid guidValue = default; + Guid nullableGuidValue = default; + DateTime dateTimeValue = default; + DateTime nullableDateTimeValue = default; + DateTimeOffset dateTimeOffsetValue = default; + DateTimeOffset nullableDateTimeOffsetValue = default; + TimeSpan timeSpanValue = default; + TimeSpan nullableTimeSpanValue = default; + const DayOfWeek enumValue = default; + const DayOfWeek nullableEnumValue = default; + + var document = new Document + { + Data = new SingleOrManyData(new ResourceObject + { + Type = "resourceWithVariousDataTypes", + Attributes = new Dictionary + { + ["boolean"] = booleanValue, + ["nullableBoolean"] = nullableBooleanValue, + ["char"] = charValue, + ["nullableChar"] = nullableCharValue, + ["unsignedLong"] = unsignedLongValue, + ["nullableUnsignedLong"] = nullableUnsignedLongValue, + ["decimal"] = decimalValue, + ["nullableDecimal"] = nullableDecimalValue, + ["float"] = floatValue, + ["nullableFloat"] = nullableFloatValue, + ["string"] = stringValue, + ["guid"] = guidValue, + ["nullableGuid"] = nullableGuidValue, + ["dateTime"] = dateTimeValue, + ["nullableDateTime"] = nullableDateTimeValue, + ["dateTimeOffset"] = dateTimeOffsetValue, + ["nullableDateTimeOffset"] = nullableDateTimeOffsetValue, + ["timeSpan"] = timeSpanValue, + ["nullableTimeSpan"] = nullableTimeSpanValue, + ["enum"] = enumValue, + ["nullableEnum"] = nullableEnumValue, + ["complexObject"] = null, + ["complexObjectList"] = null + } + }) + }; + + // Act + var model = (ResourceWithVariousDataTypes)documentAdapter.Convert(document); + + // Assert + model.Should().NotBeNull(); + + model.Boolean.Should().Be(booleanValue); + model.NullableBoolean.Should().Be(nullableBooleanValue); + model.Char.Should().Be(charValue); + model.NullableChar.Should().Be(nullableCharValue); + model.UnsignedLong.Should().Be(unsignedLongValue); + model.NullableUnsignedLong.Should().Be(nullableUnsignedLongValue); + model.Decimal.Should().Be(decimalValue); + model.NullableDecimal.Should().Be(nullableDecimalValue); + model.Float.Should().Be(floatValue); + model.NullableFloat.Should().Be(nullableFloatValue); + model.String.Should().Be(stringValue); + model.Guid.Should().Be(guidValue); + model.NullableGuid.Should().Be(nullableGuidValue); + model.DateTime.Should().Be(dateTimeValue); + model.NullableDateTime.Should().Be(nullableDateTimeValue); + model.DateTimeOffset.Should().Be(dateTimeOffsetValue); + model.NullableDateTimeOffset.Should().Be(nullableDateTimeOffsetValue); + model.TimeSpan.Should().Be(timeSpanValue); + model.NullableTimeSpan.Should().Be(nullableTimeSpanValue); + model.Enum.Should().Be(enumValue); + model.NullableEnum.Should().Be(nullableEnumValue); + model.ComplexObject.Should().BeNull(); + model.ComplexObjectList.Should().BeNull(); + } + + private static DocumentAdapter CreateDocumentAdapter(Func createRequest) + where TResource : Identifiable + { + var options = new JsonApiOptions(); + IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); + options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph)); + + var serviceContainer = new ServiceContainer(); + var resourceFactory = new ResourceFactory(serviceContainer); + var resourceDefinitionAccessor = new ResourceDefinitionAccessor(resourceGraph, serviceContainer); + + serviceContainer.AddService(typeof(IResourceDefinitionAccessor), resourceDefinitionAccessor); + serviceContainer.AddService(typeof(IResourceDefinition), new JsonApiResourceDefinition(resourceGraph)); + + JsonApiRequest request = createRequest(resourceGraph); + var targetedFields = new TargetedFields(); + + var resourceIdentifierObjectAdapter = new ResourceIdentifierObjectAdapter(resourceGraph, resourceFactory); + var relationshipDataAdapter = new RelationshipDataAdapter(resourceGraph, resourceIdentifierObjectAdapter); + var resourceObjectAdapter = new ResourceObjectAdapter(resourceGraph, resourceFactory, options, relationshipDataAdapter); + var resourceDataAdapter = new ResourceDataAdapter(resourceDefinitionAccessor, resourceObjectAdapter); + + var atomicReferenceAdapter = new AtomicReferenceAdapter(resourceGraph, resourceFactory); + var atomicOperationResourceDataAdapter = new OperationResourceDataAdapter(resourceDefinitionAccessor, resourceObjectAdapter); + + var atomicOperationObjectAdapter = new AtomicOperationObjectAdapter(resourceGraph, options, atomicReferenceAdapter, + atomicOperationResourceDataAdapter, relationshipDataAdapter); + + var resourceDocumentAdapter = new ResourceDocumentAdapter(options, resourceDataAdapter, relationshipDataAdapter); + var operationsDocumentAdapter = new OperationsDocumentAdapter(options, atomicOperationObjectAdapter); + + return new DocumentAdapter(request, targetedFields, resourceDocumentAdapter, operationsDocumentAdapter); + } + + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class ResourceWithVariousDataTypes : Identifiable + { + [Attr] + public bool Boolean { get; set; } + + [Attr] + public bool? NullableBoolean { get; set; } + + [Attr] + public char Char { get; set; } + + [Attr] + public char? NullableChar { get; set; } + + [Attr] + public ulong UnsignedLong { get; set; } + + [Attr] + public ulong? NullableUnsignedLong { get; set; } + + [Attr] + public decimal Decimal { get; set; } + + [Attr] + public decimal? NullableDecimal { get; set; } + + [Attr] + public float Float { get; set; } + + [Attr] + public float? NullableFloat { get; set; } + + [Attr] + public string String { get; set; } + + [Attr] + public Guid Guid { get; set; } + + [Attr] + public Guid? NullableGuid { get; set; } + + [Attr] + public DateTime DateTime { get; set; } + + [Attr] + public DateTime? NullableDateTime { get; set; } + + [Attr] + public DateTimeOffset DateTimeOffset { get; set; } + + [Attr] + public DateTimeOffset? NullableDateTimeOffset { get; set; } + + [Attr] + public TimeSpan TimeSpan { get; set; } + + [Attr] + public TimeSpan? NullableTimeSpan { get; set; } + + [Attr] + public DayOfWeek Enum { get; set; } + + [Attr] + public DayOfWeek? NullableEnum { get; set; } + + [Attr] + public ComplexObject ComplexObject { get; set; } + + [Attr] + public IList ComplexObjectList { get; set; } + } + + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class ComplexObject + { + public string Value { get; set; } + } + } +} diff --git a/test/TestBuildingBlocks/IntegrationTestContext.cs b/test/TestBuildingBlocks/IntegrationTestContext.cs index 68550f4c26..fc4506b254 100644 --- a/test/TestBuildingBlocks/IntegrationTestContext.cs +++ b/test/TestBuildingBlocks/IntegrationTestContext.cs @@ -102,7 +102,7 @@ private WebApplicationFactory CreateFactory() // We have placed an appsettings.json in the TestBuildingBlock project folder and set the content root to there. Note that controllers // are not discovered in the content root but are registered manually using IntegrationTestContext.UseController. WebApplicationFactory factoryWithConfiguredContentRoot = - factory.WithWebHostBuilder(builder => builder.UseSolutionRelativeContentRoot("test/" + nameof(TestBuildingBlocks))); + factory.WithWebHostBuilder(builder => builder.UseSolutionRelativeContentRoot($"test/{nameof(TestBuildingBlocks)}")); using IServiceScope scope = factoryWithConfiguredContentRoot.Services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); diff --git a/test/UnitTests/Models/ResourceConstructionTests.cs b/test/UnitTests/Models/ResourceConstructionTests.cs deleted file mode 100644 index 81d08fe548..0000000000 --- a/test/UnitTests/Models/ResourceConstructionTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.ComponentModel.Design; -using System.Text.Json; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; - -namespace UnitTests.Models -{ - public sealed class ResourceConstructionTests - { - private readonly Mock _requestMock; - private readonly Mock _mockHttpContextAccessor; - private readonly Mock _resourceDefinitionAccessorMock = new(); - - public ResourceConstructionTests() - { - _mockHttpContextAccessor = new Mock(); - _mockHttpContextAccessor.Setup(mock => mock.HttpContext).Returns(new DefaultHttpContext()); - _requestMock = new Mock(); - _requestMock.Setup(mock => mock.Kind).Returns(EndpointKind.Primary); - } - - [Fact] - public void When_resource_has_default_constructor_it_must_succeed() - { - // Arrange - var options = new JsonApiOptions(); - - IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); - - var serviceContainer = new ServiceContainer(); - serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor()); - - var serializer = new RequestDeserializer(resourceGraph, new ResourceFactory(serviceContainer), new TargetedFields(), - _mockHttpContextAccessor.Object, _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); - - var body = new - { - data = new - { - id = "1", - type = "resourceWithoutConstructors" - } - }; - - string content = JsonSerializer.Serialize(body); - - // Act - object result = serializer.Deserialize(content); - - // Assert - Assert.NotNull(result); - Assert.Equal(typeof(ResourceWithoutConstructor), result.GetType()); - } - - [Fact] - public void When_resource_has_default_constructor_that_throws_it_must_fail() - { - // Arrange - var options = new JsonApiOptions(); - - IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); - - var serviceContainer = new ServiceContainer(); - serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor()); - - var serializer = new RequestDeserializer(resourceGraph, new ResourceFactory(serviceContainer), new TargetedFields(), - _mockHttpContextAccessor.Object, _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); - - var body = new - { - data = new - { - id = "1", - type = "resourceWithThrowingConstructors" - } - }; - - string content = JsonSerializer.Serialize(body); - - // Act - Action action = () => serializer.Deserialize(content); - - // Assert - var exception = Assert.Throws(action); - - Assert.Equal("Failed to create an instance of 'UnitTests.Models.ResourceWithThrowingConstructor' using its default constructor.", - exception.Message); - } - - [Fact] - public void When_resource_has_constructor_with_string_parameter_it_must_fail() - { - // Arrange - var options = new JsonApiOptions(); - - IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); - - var serviceContainer = new ServiceContainer(); - serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor()); - - var serializer = new RequestDeserializer(resourceGraph, new ResourceFactory(serviceContainer), new TargetedFields(), - _mockHttpContextAccessor.Object, _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); - - var body = new - { - data = new - { - id = "1", - type = "resourceWithStringConstructors" - } - }; - - string content = JsonSerializer.Serialize(body); - - // Act - Action action = () => serializer.Deserialize(content); - - // Assert - var exception = Assert.Throws(action); - - Assert.Equal("Failed to create an instance of 'UnitTests.Models.ResourceWithStringConstructor' using injected constructor parameters.", - exception.Message); - } - } -} diff --git a/test/UnitTests/NeverResourceDefinitionAccessor.cs b/test/UnitTests/NeverResourceDefinitionAccessor.cs deleted file mode 100644 index f4d0f308bf..0000000000 --- a/test/UnitTests/NeverResourceDefinitionAccessor.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace UnitTests -{ - internal sealed class NeverResourceDefinitionAccessor : IResourceDefinitionAccessor - { - public IImmutableList OnApplyIncludes(Type resourceType, IImmutableList existingIncludes) - { - return existingIncludes; - } - - public FilterExpression OnApplyFilter(Type resourceType, FilterExpression existingFilter) - { - return existingFilter; - } - - public SortExpression OnApplySort(Type resourceType, SortExpression existingSort) - { - return existingSort; - } - - public PaginationExpression OnApplyPagination(Type resourceType, PaginationExpression existingPagination) - { - return existingPagination; - } - - public SparseFieldSetExpression OnApplySparseFieldSet(Type resourceType, SparseFieldSetExpression existingSparseFieldSet) - { - return existingSparseFieldSet; - } - - public object GetQueryableHandlerForQueryStringParameter(Type resourceType, string parameterName) - { - return new QueryStringParameterHandlers(); - } - - public IDictionary GetMeta(Type resourceType, IIdentifiable resourceInstance) - { - return null; - } - - public Task OnPrepareWriteAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnSetToOneRelationshipAsync(TResource leftResource, HasOneAttribute hasOneRelationship, - IIdentifiable rightResourceId, WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.FromResult(rightResourceId); - } - - public Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnAddToRelationshipAsync(TId leftResourceId, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnRemoveFromRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnWritingAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnWriteSucceededAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public void OnDeserialize(IIdentifiable resource) - { - } - - public void OnSerialize(IIdentifiable resource) - { - } - } -} diff --git a/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs b/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs deleted file mode 100644 index 6e6b417af9..0000000000 --- a/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs +++ /dev/null @@ -1,451 +0,0 @@ -using System; -using System.Collections.Generic; -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 UnitTests.TestModels; -using Xunit; - -namespace UnitTests.Serialization.Common -{ - public sealed class BaseDocumentParserTests : DeserializerTestsSetup - { - private readonly TestDeserializer _deserializer; - - public BaseDocumentParserTests() - { - _deserializer = new TestDeserializer(ResourceGraph, new ResourceFactory(new ServiceContainer()), Options); - } - - [Fact] - public void DeserializeResourceIdentifiers_SingleData_CanDeserialize() - { - // Arrange - var content = new Document - { - Data = new SingleOrManyData(new ResourceObject - { - Type = "testResource", - Id = "1" - }) - }; - - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (TestResource)_deserializer.Deserialize(body); - - // Assert - Assert.Equal(1, result.Id); - } - - [Fact] - public void DeserializeResourceIdentifiers_EmptySingleData_CanDeserialize() - { - // Arrange - var content = new Document(); - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - object result = _deserializer.Deserialize(body); - - // Arrange - Assert.Null(result); - } - - [Fact] - public void DeserializeResourceIdentifiers_ArrayData_CanDeserialize() - { - // Arrange - var content = new Document - { - Data = new SingleOrManyData(new List - { - new() - { - Type = "testResource", - Id = "1" - } - }) - }; - - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (IEnumerable)_deserializer.Deserialize(body); - - // Assert - Assert.Equal("1", result.First().StringId); - } - - [Fact] - public void DeserializeResourceIdentifiers_EmptyArrayData_CanDeserialize() - { - var content = new Document - { - Data = new SingleOrManyData(Array.Empty()) - }; - - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (IEnumerable)_deserializer.Deserialize(body); - - // Assert - Assert.Empty(result); - } - - [Theory] - [InlineData("stringField", "some string")] - [InlineData("stringField", null)] - [InlineData("intField", null, true)] - [InlineData("intField", 1)] - [InlineData("intField", "1", true)] - [InlineData("nullableIntField", null)] - [InlineData("nullableIntField", 1)] - [InlineData("guidField", "bad format", true)] - [InlineData("guidField", "1a68be43-cc84-4924-a421-7f4d614b7781")] - [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) - { - // Arrange - var content = new Document - { - Data = new SingleOrManyData(new ResourceObject - { - Type = "testResource", - Id = "1", - Attributes = new Dictionary - { - [member] = value - } - }) - }; - - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - Func action = () => (TestResource)_deserializer.Deserialize(body); - - // Assert - if (expectError) - { - Assert.ThrowsAny(action); - } - else - { - TestResource resource = action(); - - PropertyInfo pi = ResourceGraph.GetResourceContext("testResource").GetAttributeByPublicName(member).Property; - object deserializedValue = pi.GetValue(resource); - - if (member == "intField") - { - Assert.Equal(1, deserializedValue); - } - else if (member == "nullableIntField" && value == null) - { - Assert.Null(deserializedValue); - } - else if (member == "nullableIntField" && (int?)value == 1) - { - Assert.Equal(1, deserializedValue); - } - else if (member == "guidField") - { - Assert.Equal(deserializedValue, Guid.Parse("1a68be43-cc84-4924-a421-7f4d614b7781")); - } - else if (member == "dateTimeField") - { - Assert.Equal(deserializedValue, DateTime.Parse("9/11/2019 11:41:40 AM")); - } - else - { - Assert.Equal(value, deserializedValue); - } - } - } - - [Fact] - public void DeserializeAttributes_ComplexType_CanDeserialize() - { - // Arrange - var content = new Document - { - Data = new SingleOrManyData(new ResourceObject - { - Type = "testResource", - Id = "1", - Attributes = new Dictionary - { - ["complexField"] = new Dictionary - { - ["compoundName"] = "testName" - } - } - }) - }; - - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (TestResource)_deserializer.Deserialize(body); - - // Assert - Assert.NotNull(result.ComplexField); - Assert.Equal("testName", result.ComplexField.CompoundName); - } - - [Fact] - public void DeserializeAttributes_ComplexListType_CanDeserialize() - { - // Arrange - var content = new Document - { - Data = new SingleOrManyData(new ResourceObject - { - Type = "testResource-with-list", - Id = "1", - Attributes = new Dictionary - { - ["complexFields"] = new[] - { - new Dictionary - { - ["compoundName"] = "testName" - } - } - } - }) - }; - - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (TestResourceWithList)_deserializer.Deserialize(body); - - // Assert - Assert.NotNull(result.ComplexFields); - Assert.NotEmpty(result.ComplexFields); - Assert.Equal("testName", result.ComplexFields[0].CompoundName); - } - - [Fact] - public void DeserializeRelationship_SingleDataForToOneRelationship_CannotDeserialize() - { - // Arrange - Document content = CreateDocumentWithRelationships("oneToManyPrincipals", "dependents"); - - content.Data.SingleValue.Relationships["dependents"] = new RelationshipObject - { - Data = new SingleOrManyData(new ResourceIdentifierObject - { - Type = "Dependents", - Id = "1" - }) - }; - - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - Action action = () => _deserializer.Deserialize(body); - - // Assert - Assert.Throws(action); - } - - [Fact] - public void DeserializeRelationship_ManyDataForToManyRelationship_CannotDeserialize() - { - // Arrange - Document content = CreateDocumentWithRelationships("oneToOnePrincipals", "dependent"); - - content.Data.SingleValue.Relationships["dependent"] = new RelationshipObject - { - Data = new SingleOrManyData(new List - { - new() - { - Type = "Dependent", - Id = "1" - } - }) - }; - - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - Action action = () => _deserializer.Deserialize(body); - - // Assert - Assert.Throws(action); - } - - [Fact] - public void DeserializeRelationships_EmptyOneToOneDependent_NavigationPropertyIsNull() - { - // Arrange - Document content = CreateDocumentWithRelationships("oneToOnePrincipals", "dependent"); - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (OneToOnePrincipal)_deserializer.Deserialize(body); - - // Assert - Assert.Equal(1, result.Id); - Assert.Null(result.Dependent); - Assert.Null(result.AttributeMember); - } - - [Fact] - public void DeserializeRelationships_PopulatedOneToOneDependent_NavigationPropertyIsPopulated() - { - // Arrange - Document content = CreateDocumentWithRelationships("oneToOnePrincipals", "dependent", "oneToOneDependents"); - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (OneToOnePrincipal)_deserializer.Deserialize(body); - - // Assert - Assert.Equal(1, result.Id); - Assert.Equal(10, result.Dependent.Id); - Assert.Null(result.AttributeMember); - } - - [Fact] - public void DeserializeRelationships_EmptyOneToOnePrincipal_NavigationIsNull() - { - // Arrange - Document content = CreateDocumentWithRelationships("oneToOneDependents", "principal"); - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (OneToOneDependent)_deserializer.Deserialize(body); - - // Assert - Assert.Equal(1, result.Id); - Assert.Null(result.Principal); - } - - [Fact] - public void DeserializeRelationships_EmptyRequiredOneToOnePrincipal_NavigationIsNull() - { - // Arrange - Document content = CreateDocumentWithRelationships("oneToOneRequiredDependents", "principal"); - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (OneToOneRequiredDependent)_deserializer.Deserialize(body); - - // Assert - Assert.Equal(1, result.Id); - Assert.Null(result.Principal); - } - - [Fact] - public void DeserializeRelationships_PopulatedOneToOnePrincipal_NavigationIsPopulated() - { - // Arrange - Document content = CreateDocumentWithRelationships("oneToOneDependents", "principal", "oneToOnePrincipals"); - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (OneToOneDependent)_deserializer.Deserialize(body); - - // Assert - Assert.Equal(1, result.Id); - Assert.NotNull(result.Principal); - Assert.Equal(10, result.Principal.Id); - Assert.Null(result.AttributeMember); - } - - [Fact] - public void DeserializeRelationships_EmptyOneToManyPrincipal_NavigationIsNull() - { - // Arrange - Document content = CreateDocumentWithRelationships("oneToManyDependents", "principal"); - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (OneToManyDependent)_deserializer.Deserialize(body); - - // Assert - Assert.Equal(1, result.Id); - Assert.Null(result.Principal); - Assert.Null(result.AttributeMember); - } - - [Fact] - public void DeserializeRelationships_EmptyOneToManyRequiredPrincipal_NavigationIsNull() - { - // Arrange - Document content = CreateDocumentWithRelationships("oneToMany-requiredDependents", "principal"); - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (OneToManyRequiredDependent)_deserializer.Deserialize(body); - - // Assert - Assert.Equal(1, result.Id); - Assert.Null(result.Principal); - Assert.Null(result.AttributeMember); - } - - [Fact] - public void DeserializeRelationships_PopulatedOneToManyPrincipal_NavigationIsPopulated() - { - // Arrange - Document content = CreateDocumentWithRelationships("oneToManyDependents", "principal", "oneToManyPrincipals"); - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (OneToManyDependent)_deserializer.Deserialize(body); - - // Assert - Assert.Equal(1, result.Id); - Assert.NotNull(result.Principal); - Assert.Equal(10, result.Principal.Id); - Assert.Null(result.AttributeMember); - } - - [Fact] - public void DeserializeRelationships_EmptyOneToManyDependent_NavigationIsNull() - { - // Arrange - Document content = CreateDocumentWithRelationships("oneToManyPrincipals", "dependents", isToManyData: true); - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (OneToManyPrincipal)_deserializer.Deserialize(body); - - // Assert - Assert.Equal(1, result.Id); - Assert.Empty(result.Dependents); - Assert.Null(result.AttributeMember); - } - - [Fact] - public void DeserializeRelationships_PopulatedOneToManyDependent_NavigationIsPopulated() - { - // Arrange - Document content = CreateDocumentWithRelationships("oneToManyPrincipals", "dependents", "oneToManyDependents", true); - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - var result = (OneToManyPrincipal)_deserializer.Deserialize(body); - - // Assert - Assert.Equal(1, result.Id); - Assert.Single(result.Dependents); - Assert.Equal(10, result.Dependents.First().Id); - Assert.Null(result.AttributeMember); - } - } -} diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs deleted file mode 100644 index 5e1ae08546..0000000000 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ /dev/null @@ -1,119 +0,0 @@ -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; - -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()); - } - - protected Document CreateDocumentWithRelationships(string primaryType, string relationshipMemberName, string relatedType = null, - bool isToManyData = false) - { - Document content = CreateDocumentWithRelationships(primaryType); - content.Data.SingleValue.Relationships.Add(relationshipMemberName, CreateRelationshipData(relatedType, isToManyData)); - return content; - } - - protected Document CreateDocumentWithRelationships(string primaryType) - { - return new() - { - Data = new SingleOrManyData(new ResourceObject - { - Id = "1", - Type = primaryType, - Relationships = new Dictionary() - }) - }; - } - - protected RelationshipObject CreateRelationshipData(string relatedType = null, bool isToManyData = false, string id = "10") - { - var relationshipObject = new RelationshipObject(); - - ResourceIdentifierObject rio = relatedType == null - ? null - : new ResourceIdentifierObject - { - Id = id, - Type = relatedType - }; - - if (isToManyData) - { - IList rios = relatedType != null ? rio.AsList() : Array.Empty(); - relationshipObject.Data = new SingleOrManyData(rios); - } - else - { - relationshipObject.Data = new SingleOrManyData(rio); - } - - return relationshipObject; - } - - protected Document CreateTestResourceDocument() - { - return new() - { - Data = new SingleOrManyData(new ResourceObject - { - Type = "testResource", - Id = "1", - Attributes = new Dictionary - { - ["stringField"] = "some string", - ["intField"] = 1, - ["nullableIntField"] = null, - ["guidField"] = "1a68be43-cc84-4924-a421-7f4d614b7781", - ["dateTimeField"] = DateTime.Parse("9/11/2019 11:41:40 AM", CultureInfo.InvariantCulture) - } - }) - }; - } - - protected sealed class TestDeserializer : BaseDeserializer - { - private readonly IJsonApiOptions _options; - - public TestDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory, IJsonApiOptions options) - : base(resourceGraph, resourceFactory, options) - { - _options = options; - } - - public object Deserialize(string body) - { - return DeserializeData(body, _options.SerializerReadOptions); - } - - protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipObject data = null) - { - } - } - } -} diff --git a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs deleted file mode 100644 index c6f8747eba..0000000000 --- a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -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 Xunit; - -namespace UnitTests.Serialization.Server -{ - public sealed class RequestDeserializerTests : DeserializerTestsSetup - { - private readonly RequestDeserializer _deserializer; - private readonly Mock _fieldsManagerMock = new(); - private readonly Mock _requestMock = new(); - private readonly Mock _resourceDefinitionAccessorMock = new(); - - 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, options, _resourceDefinitionAccessorMock.Object); - } - - [Fact] - public void DeserializeAttributes_VariousUpdatedMembers_RegistersTargetedFields() - { - // Arrange - var attributesToUpdate = new HashSet(); - var relationshipsToUpdate = new HashSet(); - SetupFieldsManager(attributesToUpdate, relationshipsToUpdate); - - Document content = CreateTestResourceDocument(); - - string body = JsonSerializer.Serialize(content, SerializerWriteOptions); - - // Act - _deserializer.Deserialize(body); - - // Assert - Assert.Equal(5, attributesToUpdate.Count); - Assert.Empty(relationshipsToUpdate); - } - - [Fact] - public void DeserializeRelationships_MultipleDependentRelationships_RegistersUpdatedRelationships() - { - // Arrange - var attributesToUpdate = new HashSet(); - var relationshipsToUpdate = new HashSet(); - SetupFieldsManager(attributesToUpdate, relationshipsToUpdate); - - Document content = CreateDocumentWithRelationships("multiPrincipals"); - 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); - - // Assert - Assert.Equal(4, relationshipsToUpdate.Count); - Assert.Empty(attributesToUpdate); - } - - [Fact] - public void DeserializeRelationships_MultiplePrincipalRelationships_RegistersUpdatedRelationships() - { - // Arrange - var attributesToUpdate = new HashSet(); - var relationshipsToUpdate = new HashSet(); - SetupFieldsManager(attributesToUpdate, relationshipsToUpdate); - - Document content = CreateDocumentWithRelationships("multiDependents"); - 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); - - // Assert - Assert.Equal(4, relationshipsToUpdate.Count); - Assert.Empty(attributesToUpdate); - } - - private void SetupFieldsManager(HashSet attributesToUpdate, HashSet relationshipsToUpdate) - { - _fieldsManagerMock.Setup(fields => fields.Attributes).Returns(attributesToUpdate); - _fieldsManagerMock.Setup(fields => fields.Relationships).Returns(relationshipsToUpdate); - } - } -} diff --git a/test/UnitTests/TestResourceFactory.cs b/test/UnitTests/TestResourceFactory.cs deleted file mode 100644 index 8df6c2e4b1..0000000000 --- a/test/UnitTests/TestResourceFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Linq.Expressions; -using JsonApiDotNetCore.Resources; - -namespace UnitTests -{ - internal sealed class TestResourceFactory : IResourceFactory - { - public IIdentifiable CreateInstance(Type resourceType) - { - return (IIdentifiable)Activator.CreateInstance(resourceType); - } - - public TResource CreateInstance() - where TResource : IIdentifiable - { - return (TResource)Activator.CreateInstance(typeof(TResource)); - } - - public NewExpression CreateNewExpression(Type resourceType) - { - return Expression.New(resourceType); - } - } -} From d247ba230ec878fa62a0a3ca24acb5ef0fd4363e Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 29 Sep 2021 16:04:44 +0200 Subject: [PATCH 07/49] Updated error message for unknown attribute/relationship --- .../Errors/InvalidRequestBodyException.cs | 16 +++------ .../Serialization/JsonApiReader.cs | 8 +++-- .../Serialization/ModelConversionException.cs | 25 +++++++++++++ .../RequestAdapters/AtomicReferenceAdapter.cs | 11 +----- .../ResourceIdentityAdapter.cs | 11 ++++++ .../RequestAdapters/ResourceObjectAdapter.cs | 36 +++++-------------- .../Creating/AtomicCreateResourceTests.cs | 8 ++--- .../AtomicAddToToManyRelationshipTests.cs | 4 +-- ...AtomicRemoveFromToManyRelationshipTests.cs | 4 +-- .../AtomicReplaceToManyRelationshipTests.cs | 4 +-- .../AtomicUpdateToOneRelationshipTests.cs | 4 +-- .../Resources/AtomicUpdateResourceTests.cs | 8 ++--- .../ReadWrite/Creating/CreateResourceTests.cs | 8 ++--- .../Updating/Resources/UpdateResourceTests.cs | 8 ++--- 14 files changed, 79 insertions(+), 76 deletions(-) create mode 100644 src/JsonApiDotNetCore/Serialization/ModelConversionException.cs diff --git a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs index ede05c3eb4..15fdc6e227 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs @@ -1,6 +1,5 @@ using System; using System.Net; -using System.Text; using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; @@ -14,11 +13,12 @@ public sealed class InvalidRequestBodyException : JsonApiException { public string RequestBody { get; } - public InvalidRequestBodyException(string reason, string details, string requestBody, string sourcePointer, Exception innerException = null) + public InvalidRequestBodyException(string requestBody, string genericMessage, string specificMessage, string sourcePointer, + Exception innerException = null) : base(new ErrorObject(HttpStatusCode.UnprocessableEntity) { - Title = reason != null ? $"Failed to deserialize request body: {reason}" : "Failed to deserialize request body.", - Detail = FormatErrorDetail(details, innerException), + Title = genericMessage != null ? $"Failed to deserialize request body: {genericMessage}" : "Failed to deserialize request body.", + Detail = specificMessage, Source = sourcePointer == null ? null : new ErrorSource @@ -29,13 +29,5 @@ public InvalidRequestBodyException(string reason, string details, string request { RequestBody = requestBody; } - - private static string FormatErrorDetail(string details, Exception innerException) - { - var builder = new StringBuilder(); - builder.Append(details ?? innerException?.Message); - - return builder.Length > 0 ? builder.ToString() : null; - } } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index 2f2fa4faaa..4f7897bb2c 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -69,9 +69,13 @@ private object GetModel(string requestBody) { return _documentAdapter.Convert(document); } + catch (ModelConversionException exception) + { + throw new InvalidRequestBodyException(requestBody, exception.GenericMessage, exception.SpecificMessage, exception.SourcePointer, exception); + } catch (DeserializationException exception) { - throw new InvalidRequestBodyException(exception.GenericMessage, exception.SpecificMessage, requestBody, exception.SourcePointer); + throw new InvalidRequestBodyException(requestBody, exception.GenericMessage, exception.SpecificMessage, exception.SourcePointer); } } @@ -103,7 +107,7 @@ private Document DeserializeDocument(string requestBody, JsonSerializerOptions s // 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 InvalidRequestBodyException(null, exception.Message, requestBody, null, exception); + throw new InvalidRequestBodyException(requestBody, null, exception.Message, null, exception); } } } diff --git a/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs b/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs new file mode 100644 index 0000000000..024e0a75ba --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs @@ -0,0 +1,25 @@ +using System; +using JsonApiDotNetCore.Serialization.RequestAdapters; + +namespace JsonApiDotNetCore.Serialization +{ + /// + /// The error that is thrown when unable to convert a deserialized request body to an ASP.NET model. + /// + internal sealed class ModelConversionException : Exception + { + public string GenericMessage { get; } + public string SpecificMessage { get; } + public string SourcePointer { get; } + + public ModelConversionException(RequestAdapterPosition position, string genericMessage, string specificMessage) + : base(genericMessage) + { + ArgumentGuard.NotNull(position, nameof(position)); + + GenericMessage = genericMessage; + SpecificMessage = specificMessage; + SourcePointer = position.ToSourcePointer(); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs index 5f4dffdd17..5da4500b24 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs @@ -39,21 +39,12 @@ private RelationshipAttribute ConvertRelationship(string relationshipName, Resou RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(relationshipName); - AssertIsKnownRelationship(relationship, relationshipName, state); + AssertIsKnownRelationship(relationship, relationshipName, resourceContext, state); AssertToManyInAddOrRemoveRelationship(relationship, state); return relationship; } - private static void AssertIsKnownRelationship(RelationshipAttribute relationship, string relationshipName, RequestAdapterState state) - { - if (relationship == null) - { - throw new DeserializationException(state.Position, "Request body includes unknown relationship.", - $"Relationship '{relationshipName}' does not exist."); - } - } - private static void AssertToManyInAddOrRemoveRelationship(RelationshipAttribute relationship, RequestAdapterState state) { bool requireToManyRelationship = state.Request.WriteOperation == WriteOperationKind.AddToRelationship || diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs index 24819d1b71..4d4385cdbb 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs @@ -4,6 +4,7 @@ using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Serialization.RequestAdapters @@ -212,5 +213,15 @@ private string ConvertLid(IResourceIdentity identity, RequestAdapterState state) return identity.Lid; } + + protected static void AssertIsKnownRelationship(RelationshipAttribute relationship, string relationshipName, ResourceContext resourceContext, + RequestAdapterState state) + { + if (relationship == null) + { + throw new ModelConversionException(state.Position, "Unknown relationship found.", + $"Relationship '{relationshipName}' does not exist on resource type '{resourceContext.PublicName}'."); + } + } } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs index ba304e2b4e..d501972114 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs @@ -60,11 +60,12 @@ private void ConvertAttribute(IIdentifiable resource, string attributeName, obje AttrAttribute attr = resourceContext.TryGetAttributeByPublicName(attributeName); - if (!AssertIsKnownAttribute(attr, attributeName, state)) + if (attr == null && _options.AllowUnknownFieldsInRequestBody) { return; } + AssertIsKnownAttribute(attr, attributeName, resourceContext, state); AssertNoInvalidAttribute(attributeValue, state); AssertNoBlockedCreate(attr, state); AssertNoBlockedChange(attr, state); @@ -75,19 +76,13 @@ private void ConvertAttribute(IIdentifiable resource, string attributeName, obje } [AssertionMethod] - private bool AssertIsKnownAttribute(AttrAttribute attr, string attributeName, RequestAdapterState state) + private static void AssertIsKnownAttribute(AttrAttribute attr, string attributeName, ResourceContext resourceContext, RequestAdapterState state) { if (attr == null) { - if (_options.AllowUnknownFieldsInRequestBody) - { - return false; - } - - throw new DeserializationException(state.Position, "Request body includes unknown attribute.", $"Attribute '{attributeName}' does not exist."); + throw new ModelConversionException(state.Position, "Unknown attribute found.", + $"Attribute '{attributeName}' does not exist on resource type '{resourceContext.PublicName}'."); } - - return true; } private static void AssertNoInvalidAttribute(object attributeValue, RequestAdapterState state) @@ -150,32 +145,17 @@ private void ConvertRelationship(string relationshipName, SingleOrManyData ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown attribute."); - error.Detail.Should().Be("Attribute 'doesNotExist' does not exist."); + error.Title.Should().Be("Failed to deserialize request body: Unknown attribute found."); + error.Detail.Should().Be("Attribute 'doesNotExist' does not exist on resource type 'musicTracks'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); @@ -320,8 +320,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship."); - error.Detail.Should().Be("Relationship 'doesNotExist' does not exist."); + error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); + error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource type 'musicTracks'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index d1fd2fbfad..ef8ac4c458 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -284,8 +284,8 @@ public async Task Cannot_create_resource_with_unknown_attribute() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown attribute."); - error.Detail.Should().Be("Attribute 'doesNotExist' does not exist."); + error.Title.Should().Be("Failed to deserialize request body: Unknown attribute found."); + error.Detail.Should().Be("Attribute 'doesNotExist' does not exist on resource type 'workItems'."); error.Source.Pointer.Should().Be("/data/attributes/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); @@ -371,8 +371,8 @@ public async Task Cannot_create_resource_with_unknown_relationship() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship."); - error.Detail.Should().Be("Relationship 'doesNotExist' does not exist."); + error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); + error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource type 'workItems'."); error.Source.Pointer.Should().Be("/data/relationships/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index bf7f409b99..2979975679 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -125,8 +125,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown attribute."); - error.Detail.Should().Be("Attribute 'doesNotExist' does not exist."); + error.Title.Should().Be("Failed to deserialize request body: Unknown attribute found."); + error.Detail.Should().Be("Attribute 'doesNotExist' does not exist on resource type 'userAccounts'."); error.Source.Pointer.Should().Be("/data/attributes/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); @@ -225,8 +225,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship."); - error.Detail.Should().Be("Relationship 'doesNotExist' does not exist."); + error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); + error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource type 'userAccounts'."); error.Source.Pointer.Should().Be("/data/relationships/doesNotExist"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); From 8dbafd262cb8ebb9c91dbfdd33fe493402616b97 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 29 Sep 2021 16:11:24 +0200 Subject: [PATCH 08/49] Updated error message for unknown resource type --- .../RequestAdapters/ResourceIdentityAdapter.cs | 14 +++++++++----- .../Creating/AtomicCreateResourceTests.cs | 2 +- ...micCreateResourceWithToManyRelationshipTests.cs | 2 +- ...omicCreateResourceWithToOneRelationshipTests.cs | 2 +- .../Deleting/AtomicDeleteResourceTests.cs | 2 +- .../AtomicAddToToManyRelationshipTests.cs | 4 ++-- .../AtomicRemoveFromToManyRelationshipTests.cs | 4 ++-- .../AtomicReplaceToManyRelationshipTests.cs | 4 ++-- .../AtomicUpdateToOneRelationshipTests.cs | 4 ++-- .../AtomicReplaceToManyRelationshipTests.cs | 2 +- .../Resources/AtomicUpdateResourceTests.cs | 2 +- .../AtomicUpdateToOneRelationshipTests.cs | 2 +- .../ExceptionHandling/ExceptionHandlerTests.cs | 2 +- .../ReadWrite/Creating/CreateResourceTests.cs | 2 +- .../CreateResourceWithToManyRelationshipTests.cs | 2 +- .../CreateResourceWithToOneRelationshipTests.cs | 2 +- .../Relationships/AddToToManyRelationshipTests.cs | 2 +- .../RemoveFromToManyRelationshipTests.cs | 2 +- .../ReplaceToManyRelationshipTests.cs | 2 +- .../Relationships/UpdateToOneRelationshipTests.cs | 2 +- .../Resources/ReplaceToManyRelationshipTests.cs | 2 +- .../Updating/Resources/UpdateResourceTests.cs | 2 +- .../Resources/UpdateToOneRelationshipTests.cs | 2 +- 23 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs index 4d4385cdbb..6fbae329cf 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs @@ -143,11 +143,7 @@ state.Request.WriteOperation is WriteOperationKind.CreateResource or WriteOperat ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(identity.Type); - if (resourceContext == null) - { - throw new DeserializationException(state.Position, "Request body includes unknown resource type.", - $"Resource type '{identity.Type}' does not exist."); - } + AssertIsKnownResourceType(resourceContext, identity.Type, state); if (requirements?.ResourceContext != null && !requirements.ResourceContext.ResourceType.IsAssignableFrom(resourceContext.ResourceType)) { @@ -185,6 +181,14 @@ state.Request.WriteOperation is WriteOperationKind.CreateResource or WriteOperat return resourceContext; } + private static void AssertIsKnownResourceType(ResourceContext resourceContext, string typeName, RequestAdapterState state) + { + if (resourceContext == null) + { + throw new ModelConversionException(state.Position, "Unknown resource type found.", $"Resource type '{typeName}' does not exist."); + } + } + private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state) { if (identity.Id != null) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index f782be76ad..7fdfacb076 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -634,7 +634,7 @@ public async Task Cannot_create_resource_for_unknown_type() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index dc20d85608..c2633cb6a2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -282,7 +282,7 @@ public async Task Cannot_create_for_unknown_relationship_type() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index bce49199af..90eb904a32 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -344,7 +344,7 @@ public async Task Cannot_create_for_unknown_relationship_type() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index daba49839d..c5816cada5 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -461,7 +461,7 @@ public async Task Cannot_delete_resource_for_unknown_type() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index 0522d00f08..0135f85b80 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -338,7 +338,7 @@ public async Task Cannot_add_for_unknown_type_in_ref() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); } @@ -683,7 +683,7 @@ public async Task Cannot_add_for_unknown_type_in_data() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index 3e70a7c56f..5c176ab6a7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -338,7 +338,7 @@ public async Task Cannot_remove_for_unknown_type_in_ref() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); } @@ -646,7 +646,7 @@ public async Task Cannot_remove_for_unknown_type_in_data() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index a11170a634..f95d66d372 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -374,7 +374,7 @@ public async Task Cannot_replace_for_unknown_type_in_ref() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); } @@ -738,7 +738,7 @@ public async Task Cannot_replace_for_unknown_type_in_data() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index d9e0971e4e..8cd02d9f03 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -621,7 +621,7 @@ public async Task Cannot_create_for_unknown_type_in_ref() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); } @@ -978,7 +978,7 @@ public async Task Cannot_create_for_unknown_type_in_data() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 2aca9858df..0e27d3921f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -439,7 +439,7 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 276fee588d..21b1755750 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -1333,7 +1333,7 @@ public async Task Cannot_update_resource_for_unknown_type() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index 5074681b56..f6f8040711 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -712,7 +712,7 @@ public async Task Cannot_create_for_unknown_type_in_relationship_data() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs index 50323fbe98..8b3d6cc5d0 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs @@ -108,7 +108,7 @@ public async Task Logs_and_produces_error_response_on_deserialization_failure() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be("Resource type '' does not exist."); IEnumerable stackTraceLines = ((JsonElement)error.Meta["stackTrace"]).EnumerateArray().Select(token => token.GetString()); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index ef8ac4c458..879e21f383 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -609,7 +609,7 @@ public async Task Cannot_create_resource_for_unknown_type() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/type"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index bc8d55a99f..319d5d6745 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -402,7 +402,7 @@ public async Task Cannot_create_for_unknown_relationship_type() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]/type"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index 61ec7f3f99..b66b0d5530 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -359,7 +359,7 @@ public async Task Cannot_create_for_unknown_relationship_type() 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index 61d516b395..fe8c3874c2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -286,7 +286,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data[0]/type"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index a1f5e9d0ed..b4a74c5907 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -402,7 +402,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data[0]/type"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index c559bdcea2..7c1930fa05 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -317,7 +317,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data[0]/type"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index 865e51ef1d..98131dc11e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -344,7 +344,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/type"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index 327899f2d1..d93caf5569 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -495,7 +495,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]/type"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 2979975679..d3f53b3b5e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -817,7 +817,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/type"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index 669e75a46a..3dc353a559 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -530,7 +530,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); From d243dc3b1460a091a718aad8a05b535ae04a3e2c Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 29 Sep 2021 17:12:51 +0200 Subject: [PATCH 09/49] Unified error messages about the absense or presense of 'id' and 'lid' --- .../ForbiddenClientGeneratedIdException.cs | 25 ++++++ ...ceIdInCreateResourceNotAllowedException.cs | 27 ------ .../ResourceIdentityAdapter.cs | 88 +++++++++++-------- .../Creating/AtomicCreateResourceTests.cs | 2 +- ...reateResourceWithClientGeneratedIdTests.cs | 4 +- ...eateResourceWithToManyRelationshipTests.cs | 4 +- ...reateResourceWithToOneRelationshipTests.cs | 4 +- .../Deleting/AtomicDeleteResourceTests.cs | 8 +- .../AtomicAddToToManyRelationshipTests.cs | 16 ++-- ...AtomicRemoveFromToManyRelationshipTests.cs | 16 ++-- .../AtomicReplaceToManyRelationshipTests.cs | 16 ++-- .../AtomicUpdateToOneRelationshipTests.cs | 16 ++-- .../AtomicReplaceToManyRelationshipTests.cs | 8 +- .../Resources/AtomicUpdateResourceTests.cs | 16 ++-- .../AtomicUpdateToOneRelationshipTests.cs | 8 +- .../ReadWrite/Creating/CreateResourceTests.cs | 2 +- ...eateResourceWithToManyRelationshipTests.cs | 8 +- ...reateResourceWithToOneRelationshipTests.cs | 8 +- .../AddToToManyRelationshipTests.cs | 4 +- .../RemoveFromToManyRelationshipTests.cs | 4 +- .../ReplaceToManyRelationshipTests.cs | 4 +- .../UpdateToOneRelationshipTests.cs | 4 +- .../ReplaceToManyRelationshipTests.cs | 4 +- .../Updating/Resources/UpdateResourceTests.cs | 4 +- .../Resources/UpdateToOneRelationshipTests.cs | 4 +- 25 files changed, 156 insertions(+), 148 deletions(-) create mode 100644 src/JsonApiDotNetCore/Errors/ForbiddenClientGeneratedIdException.cs delete mode 100644 src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs diff --git a/src/JsonApiDotNetCore/Errors/ForbiddenClientGeneratedIdException.cs b/src/JsonApiDotNetCore/Errors/ForbiddenClientGeneratedIdException.cs new file mode 100644 index 0000000000..29cd67df78 --- /dev/null +++ b/src/JsonApiDotNetCore/Errors/ForbiddenClientGeneratedIdException.cs @@ -0,0 +1,25 @@ +using System.Net; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Errors +{ + /// + /// The error that is thrown when creating a resource with a client-generated ID. + /// + [PublicAPI] + public sealed class ForbiddenClientGeneratedIdException : JsonApiException + { + public ForbiddenClientGeneratedIdException(string sourcePointer) + : base(new ErrorObject(HttpStatusCode.Forbidden) + { + Title = "The use of client-generated IDs is disabled.", + Source = new ErrorSource + { + Pointer = sourcePointer + } + }) + { + } + } +} diff --git a/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs deleted file mode 100644 index a1efb23755..0000000000 --- a/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Net; -using JetBrains.Annotations; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Errors -{ - /// - /// The error that is thrown when a resource creation request or operation is received that contains a client-generated ID. - /// - [PublicAPI] - public sealed class ResourceIdInCreateResourceNotAllowedException : JsonApiException - { - public ResourceIdInCreateResourceNotAllowedException(bool isOperationsRequest, string sourcePointer) - : base(new ErrorObject(HttpStatusCode.Forbidden) - { - Title = isOperationsRequest - ? "Specifying the resource ID in operations that create a resource is not allowed." - : "Specifying the resource ID in POST resource requests is not allowed.", - Source = new ErrorSource - { - Pointer = sourcePointer - } - }) - { - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs index 6fbae329cf..fd1fc10275 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs @@ -35,36 +35,20 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory ResourceContext resourceContext = ConvertType(identity, requirements, state); - bool hasNone = identity.Id == null && identity.Lid == null; - bool hasBoth = identity.Id != null && identity.Lid != null; - - if (requirements.IdConstraint == JsonElementConstraint.Required ? hasNone || hasBoth : hasBoth) + if (state.Request.Kind != EndpointKind.AtomicOperations) { - string parent = identity is AtomicReference - ? "'ref' element" - : - requirements.RelationshipName != null && - state.Request.WriteOperation is WriteOperationKind.CreateResource or WriteOperationKind.UpdateResource - ? - $"'{requirements.RelationshipName}' relationship" - : "'data' element"; - - throw new DeserializationException(state.Position, - state.Request.Kind == EndpointKind.AtomicOperations - ? "Request body must include 'id' or 'lid' element." - : "Request body must include 'id' element.", - state.Request.Kind == EndpointKind.AtomicOperations - ? $"Expected 'id' or 'lid' element in {parent}." - : $"Expected 'id' element in {parent}."); + AssertHasNoLid(identity, state); } - if (requirements.IdConstraint == JsonElementConstraint.Forbidden && identity.Id != null) + AssertNoIdWithLid(identity, state); + + if (requirements.IdConstraint == JsonElementConstraint.Required) { - using (state.Position.PushElement("id")) - { - throw new ResourceIdInCreateResourceNotAllowedException(state.Request.Kind == EndpointKind.AtomicOperations, - state.Position.ToSourcePointer()); - } + AssertHasIdOrLid(identity, state); + } + else if (requirements.IdConstraint == JsonElementConstraint.Forbidden) + { + AssertHasNoId(identity, state); } if (requirements.IdValue != null && identity.Id != null && identity.Id != requirements.IdValue) @@ -118,7 +102,7 @@ state.Request.WriteOperation is WriteOperationKind.CreateResource or WriteOperat IIdentifiable resource = _resourceFactory.CreateInstance(resourceContext.ResourceType); AssignStringId(identity, resource, state); - resource.LocalId = ConvertLid(identity, state); + resource.LocalId = identity.Lid; return (resource, resourceContext); } @@ -189,6 +173,44 @@ private static void AssertIsKnownResourceType(ResourceContext resourceContext, s } } + private static void AssertHasNoLid(IResourceIdentity identity, RequestAdapterState state) + { + if (identity.Lid != null) + { + using IDisposable _ = state.Position.PushElement("lid"); + throw new ModelConversionException(state.Position, "The 'lid' element is not supported at this endpoint.", null); + } + } + + private static void AssertNoIdWithLid(IResourceIdentity identity, RequestAdapterState state) + { + if (identity.Id != null && identity.Lid != null) + { + throw new ModelConversionException(state.Position, "The 'id' and 'lid' element are mutually exclusive.", null); + } + } + + private static void AssertHasIdOrLid(IResourceIdentity identity, RequestAdapterState state) + { + if (identity.Id == null && identity.Lid == null) + { + string message = state.Request.Kind == EndpointKind.AtomicOperations + ? "The 'id' or 'lid' element is required." + : "The 'id' element is required."; + + throw new ModelConversionException(state.Position, message, null); + } + } + + private static void AssertHasNoId(IResourceIdentity identity, RequestAdapterState state) + { + if (identity.Id != null) + { + using IDisposable _ = state.Position.PushElement("id"); + throw new ForbiddenClientGeneratedIdException(state.Position.ToSourcePointer()); + } + } + private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state) { if (identity.Id != null) @@ -206,18 +228,6 @@ private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, } } - private string ConvertLid(IResourceIdentity identity, RequestAdapterState state) - { - using IDisposable _ = state.Position.PushElement("lid"); - - if (state.Request.Kind != EndpointKind.AtomicOperations && identity.Lid != null) - { - throw new DeserializationException(state.Position, null, "Local IDs cannot be used at this endpoint."); - } - - return identity.Lid; - } - protected static void AssertIsKnownRelationship(RelationshipAttribute relationship, string relationshipName, ResourceContext resourceContext, RequestAdapterState state) { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index 7fdfacb076..5fe4e5a732 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -459,7 +459,7 @@ public async Task Cannot_create_resource_with_client_generated_ID() 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.Title.Should().Be("The use of client-generated IDs is disabled."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs index 6c7f0518c2..21788310ba 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs @@ -263,8 +263,8 @@ public async Task Cannot_create_resource_for_ID_and_local_ID() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index c2633cb6a2..c91eade4d2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -331,8 +331,8 @@ public async Task Cannot_create_for_missing_relationship_ID() 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."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index 90eb904a32..f6d5e5a2f4 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -390,8 +390,8 @@ public async Task Cannot_create_for_missing_relationship_ID() 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."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index c5816cada5..ccd63f63bd 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -497,8 +497,8 @@ public async Task Cannot_delete_resource_for_missing_ID() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -613,8 +613,8 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index 0135f85b80..e5bf036988 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -375,8 +375,8 @@ public async Task Cannot_add_for_missing_ID_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -470,8 +470,8 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -728,8 +728,8 @@ public async Task Cannot_add_for_missing_ID_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } @@ -775,8 +775,8 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index 5c176ab6a7..ab6d6798bd 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -375,8 +375,8 @@ public async Task Cannot_remove_for_missing_ID_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -470,8 +470,8 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -691,8 +691,8 @@ public async Task Cannot_remove_for_missing_ID_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } @@ -738,8 +738,8 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index f95d66d372..ebaceee165 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -411,8 +411,8 @@ public async Task Cannot_replace_for_missing_ID_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -562,8 +562,8 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -783,8 +783,8 @@ public async Task Cannot_replace_for_missing_ID_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } @@ -830,8 +830,8 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 8cd02d9f03..74520eae21 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -658,8 +658,8 @@ public async Task Cannot_create_for_missing_ID_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -801,8 +801,8 @@ public async Task Cannot_create_for_ID_and_local_ID_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -1020,8 +1020,8 @@ public async Task Cannot_create_for_missing_ID_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } @@ -1064,8 +1064,8 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 0e27d3921f..2591456e63 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -489,8 +489,8 @@ public async Task Cannot_replace_for_missing_ID_in_relationship_data() 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."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); } @@ -541,8 +541,8 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() 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."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 21b1755750..f6611ec4be 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -782,8 +782,8 @@ public async Task Cannot_update_resource_for_missing_ID_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -831,8 +831,8 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -947,8 +947,8 @@ public async Task Cannot_update_resource_for_missing_ID_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } @@ -991,8 +991,8 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index f6f8040711..1a58409f88 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -759,8 +759,8 @@ public async Task Cannot_create_for_missing_ID_in_relationship_data() 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."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); } @@ -808,8 +808,8 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() 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."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 879e21f383..8796c3eb55 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -456,7 +456,7 @@ public async Task Cannot_create_resource_with_client_generated_ID() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("Specifying the resource ID in POST resource requests is not allowed."); + error.Title.Should().Be("The use of client-generated IDs is disabled."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/id"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index 319d5d6745..84bb41bc05 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -446,8 +446,8 @@ public async Task Cannot_create_for_missing_relationship_ID() 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().Be("Expected 'id' element in 'subscribers' relationship."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); @@ -736,8 +736,8 @@ public async Task Cannot_create_resource_with_local_ID() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().Be("Local IDs cannot be used at this endpoint."); + error.Title.Should().Be("Failed to deserialize request body: The 'lid' element is not supported at this endpoint."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/lid"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index b66b0d5530..56647f204d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -400,8 +400,8 @@ public async Task Cannot_create_for_missing_relationship_ID() 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().Be("Expected 'id' element in 'assignee' relationship."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); @@ -653,8 +653,8 @@ public async Task Cannot_create_resource_with_local_ID() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().Be("Local IDs cannot be used at this endpoint."); + error.Title.Should().Be("Failed to deserialize request body: The 'lid' element is not supported at this endpoint."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/lid"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index fe8c3874c2..8e8fa37325 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -328,8 +328,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().Be("Expected 'id' element in 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index b4a74c5907..c2b5ee4af9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -444,8 +444,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().Be("Expected 'id' element in 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index 7c1930fa05..f222371701 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -359,8 +359,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().Be("Expected 'id' element in 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index 98131dc11e..6ec536b502 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -383,8 +383,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().Be("Expected 'id' element in 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index d93caf5569..41dbd6ca4b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -548,8 +548,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().Be("Expected 'id' element in 'subscribers' relationship."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index d3f53b3b5e..32004b2577 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -856,8 +856,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().Be("Expected 'id' element in 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index 3dc353a559..df1efe7450 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -580,8 +580,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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().Be("Expected 'id' element in 'assignee' relationship."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); From 4c3bf713bdd48afde4fa51ac54016aad84508d03 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 29 Sep 2021 17:20:06 +0200 Subject: [PATCH 10/49] Unified error messages about the absense of 'type' --- .../ResourceIdentityAdapter.cs | 22 ++++++++----------- .../Creating/AtomicCreateResourceTests.cs | 4 ++-- ...eateResourceWithToManyRelationshipTests.cs | 4 ++-- ...reateResourceWithToOneRelationshipTests.cs | 4 ++-- .../Deleting/AtomicDeleteResourceTests.cs | 4 ++-- .../AtomicAddToToManyRelationshipTests.cs | 8 +++---- ...AtomicRemoveFromToManyRelationshipTests.cs | 8 +++---- .../AtomicReplaceToManyRelationshipTests.cs | 8 +++---- .../AtomicUpdateToOneRelationshipTests.cs | 8 +++---- .../AtomicReplaceToManyRelationshipTests.cs | 4 ++-- .../Resources/AtomicUpdateResourceTests.cs | 8 +++---- .../AtomicUpdateToOneRelationshipTests.cs | 4 ++-- .../ReadWrite/Creating/CreateResourceTests.cs | 4 ++-- ...eateResourceWithToManyRelationshipTests.cs | 4 ++-- ...reateResourceWithToOneRelationshipTests.cs | 4 ++-- .../AddToToManyRelationshipTests.cs | 4 ++-- .../RemoveFromToManyRelationshipTests.cs | 4 ++-- .../ReplaceToManyRelationshipTests.cs | 4 ++-- .../UpdateToOneRelationshipTests.cs | 4 ++-- .../ReplaceToManyRelationshipTests.cs | 4 ++-- .../Updating/Resources/UpdateResourceTests.cs | 4 ++-- .../Resources/UpdateToOneRelationshipTests.cs | 4 ++-- 22 files changed, 61 insertions(+), 65 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs index fd1fc10275..4be99a8736 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs @@ -109,19 +109,7 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory private ResourceContext ConvertType(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) { - if (identity.Type == null) - { - string parent = identity is AtomicReference - ? "'ref' element" - : - requirements?.RelationshipName != null && - state.Request.WriteOperation is WriteOperationKind.CreateResource or WriteOperationKind.UpdateResource - ? - $"'{requirements.RelationshipName}' relationship" - : "'data' element"; - - throw new DeserializationException(state.Position, "Request body must include 'type' element.", $"Expected 'type' element in {parent}."); - } + AssertHasType(identity, state); using IDisposable _ = state.Position.PushElement("type"); @@ -165,6 +153,14 @@ state.Request.WriteOperation is WriteOperationKind.CreateResource or WriteOperat return resourceContext; } + private static void AssertHasType(IResourceIdentity identity, RequestAdapterState state) + { + if (identity.Type == null) + { + throw new ModelConversionException(state.Position, "The 'type' element is required.", null); + } + } + private static void AssertIsKnownResourceType(ResourceContext resourceContext, string typeName, RequestAdapterState state) { if (resourceContext == null) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index 5fe4e5a732..9acb9abe56 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -598,8 +598,8 @@ public async Task Cannot_create_resource_for_missing_type() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index c91eade4d2..8e712a1137 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -232,8 +232,8 @@ public async Task Cannot_create_for_missing_relationship_type() 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."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index f6d5e5a2f4..47e6ee6238 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -297,8 +297,8 @@ public async Task Cannot_create_for_missing_relationship_type() 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."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index ccd63f63bd..9a4b70553e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -424,8 +424,8 @@ public async Task Cannot_delete_resource_for_missing_type() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index e5bf036988..00670b143b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -300,8 +300,8 @@ public async Task Cannot_add_for_missing_type_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -637,8 +637,8 @@ public async Task Cannot_add_for_missing_type_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index ab6d6798bd..7e2418ceef 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -300,8 +300,8 @@ public async Task Cannot_remove_for_missing_type_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -600,8 +600,8 @@ public async Task Cannot_remove_for_missing_type_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index ebaceee165..1183bbd0ea 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -336,8 +336,8 @@ public async Task Cannot_replace_for_missing_type_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -692,8 +692,8 @@ public async Task Cannot_replace_for_missing_type_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 74520eae21..90f9880a03 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -583,8 +583,8 @@ public async Task Cannot_create_for_missing_type_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -935,8 +935,8 @@ public async Task Cannot_create_for_missing_type_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 2591456e63..597769ddd4 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -388,8 +388,8 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() 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."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/tracks/data[0]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index f6611ec4be..9a513d67ff 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -735,8 +735,8 @@ public async Task Cannot_update_resource_for_missing_type_in_ref() 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 'ref' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } @@ -905,8 +905,8 @@ public async Task Cannot_update_resource_for_missing_type_in_data() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index 1a58409f88..8ca6ffa547 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -664,8 +664,8 @@ public async Task Cannot_create_for_missing_type_in_relationship_data() 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."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/track/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 8796c3eb55..5d82452018 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -575,8 +575,8 @@ public async Task Cannot_create_resource_for_missing_type() 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index 84bb41bc05..c5c3c76a04 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -357,8 +357,8 @@ public async Task Cannot_create_for_missing_relationship_type() 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 'subscribers' relationship."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index 56647f204d..20341b529d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -317,8 +317,8 @@ public async Task Cannot_create_for_missing_relationship_type() 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 'assignee' relationship."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index 8e8fa37325..4ef2be3689 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -243,8 +243,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index c2b5ee4af9..d1f2b3b07a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -359,8 +359,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index f222371701..756202c8d7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -274,8 +274,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index 6ec536b502..d6ad4b0af7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -304,8 +304,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index 41dbd6ca4b..488e781d4c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -441,8 +441,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'subscribers' relationship."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 32004b2577..46dd9be649 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -777,8 +777,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'data' element."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index df1efe7450..ffdeba0f47 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -479,8 +479,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'assignee' relationship."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); From 1700c00f56a1d55c43ebb5767b1e3d7d732b310e Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 29 Sep 2021 18:06:37 +0200 Subject: [PATCH 11/49] Unified error messages about incompatible types --- .../Errors/InvalidRequestBodyException.cs | 4 +- .../Errors/ResourceTypeMismatchException.cs | 25 ---------- .../Serialization/JsonApiReader.cs | 5 +- .../Serialization/ModelConversionException.cs | 5 +- .../RelationshipDataAdapter.cs | 3 +- .../ResourceIdentityAdapter.cs | 47 +++++-------------- .../ResourceIdentityRequirements.cs | 5 -- ...eateResourceWithToManyRelationshipTests.cs | 8 ++-- ...reateResourceWithToOneRelationshipTests.cs | 8 ++-- .../AtomicAddToToManyRelationshipTests.cs | 8 ++-- ...AtomicRemoveFromToManyRelationshipTests.cs | 8 ++-- .../AtomicReplaceToManyRelationshipTests.cs | 8 ++-- .../AtomicUpdateToOneRelationshipTests.cs | 8 ++-- .../AtomicReplaceToManyRelationshipTests.cs | 8 ++-- .../Resources/AtomicUpdateResourceTests.cs | 8 ++-- .../AtomicUpdateToOneRelationshipTests.cs | 8 ++-- .../ReadWrite/Creating/CreateResourceTests.cs | 2 +- ...eateResourceWithToManyRelationshipTests.cs | 8 ++-- ...reateResourceWithToOneRelationshipTests.cs | 8 ++-- .../AddToToManyRelationshipTests.cs | 2 +- .../RemoveFromToManyRelationshipTests.cs | 2 +- .../ReplaceToManyRelationshipTests.cs | 2 +- .../UpdateToOneRelationshipTests.cs | 2 +- .../ReplaceToManyRelationshipTests.cs | 8 ++-- .../Updating/Resources/UpdateResourceTests.cs | 2 +- .../Resources/UpdateToOneRelationshipTests.cs | 8 ++-- 26 files changed, 81 insertions(+), 129 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs diff --git a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs index 15fdc6e227..9051aa46d6 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs @@ -14,8 +14,8 @@ public sealed class InvalidRequestBodyException : JsonApiException public string RequestBody { get; } public InvalidRequestBodyException(string requestBody, string genericMessage, string specificMessage, string sourcePointer, - Exception innerException = null) - : base(new ErrorObject(HttpStatusCode.UnprocessableEntity) + HttpStatusCode? alternativeStatusCode = null, Exception innerException = null) + : base(new ErrorObject(alternativeStatusCode ?? HttpStatusCode.UnprocessableEntity) { Title = genericMessage != null ? $"Failed to deserialize request body: {genericMessage}" : "Failed to deserialize request body.", Detail = specificMessage, diff --git a/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs deleted file mode 100644 index 9957694d0d..0000000000 --- a/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Net; -using System.Net.Http; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Errors -{ - /// - /// The error that is thrown when the resource type in the request body does not match the type expected at the current endpoint URL. - /// - [PublicAPI] - public sealed class ResourceTypeMismatchException : JsonApiException - { - public ResourceTypeMismatchException(HttpMethod method, string requestPath, ResourceContext expected, ResourceContext actual) - : 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}'." - }) - { - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index 4f7897bb2c..6aff63ca5f 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -71,7 +71,8 @@ private object GetModel(string requestBody) } catch (ModelConversionException exception) { - throw new InvalidRequestBodyException(requestBody, exception.GenericMessage, exception.SpecificMessage, exception.SourcePointer, exception); + throw new InvalidRequestBodyException(requestBody, exception.GenericMessage, exception.SpecificMessage, exception.SourcePointer, + exception.StatusCode, exception); } catch (DeserializationException exception) { @@ -107,7 +108,7 @@ private Document DeserializeDocument(string requestBody, JsonSerializerOptions s // 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 InvalidRequestBodyException(requestBody, null, exception.Message, null, exception); + throw new InvalidRequestBodyException(requestBody, null, exception.Message, null, null, exception); } } } diff --git a/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs b/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs index 024e0a75ba..967597b0aa 100644 --- a/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs +++ b/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using JsonApiDotNetCore.Serialization.RequestAdapters; namespace JsonApiDotNetCore.Serialization @@ -10,15 +11,17 @@ internal sealed class ModelConversionException : Exception { public string GenericMessage { get; } public string SpecificMessage { get; } + public HttpStatusCode? StatusCode { get; } public string SourcePointer { get; } - public ModelConversionException(RequestAdapterPosition position, string genericMessage, string specificMessage) + public ModelConversionException(RequestAdapterPosition position, string genericMessage, string specificMessage, HttpStatusCode? statusCode = null) : base(genericMessage) { ArgumentGuard.NotNull(position, nameof(position)); GenericMessage = genericMessage; SpecificMessage = specificMessage; + StatusCode = statusCode; SourcePointer = position.ToSourcePointer(); } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs index 21517459b6..73b484e2e7 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs @@ -80,8 +80,7 @@ public object Convert(SingleOrManyData data, Relations { ResourceContext = rightResourceContext, IdConstraint = JsonElementConstraint.Required, - RelationshipName = relationship.PublicName, - UseLegacyError = state.Request.Kind != EndpointKind.Relationship + RelationshipName = relationship.PublicName }; return relationship is HasOneAttribute diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs index 4be99a8736..5ab895b21e 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs @@ -112,43 +112,10 @@ private ResourceContext ConvertType(IResourceIdentity identity, ResourceIdentity AssertHasType(identity, state); using IDisposable _ = state.Position.PushElement("type"); - ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(identity.Type); AssertIsKnownResourceType(resourceContext, identity.Type, state); - - if (requirements?.ResourceContext != null && !requirements.ResourceContext.ResourceType.IsAssignableFrom(resourceContext.ResourceType)) - { - if (requirements.UseLegacyError) - { - throw new DeserializationException(state.Position, "Relationship contains incompatible resource type.", - $"Relationship '{requirements.RelationshipName}' contains incompatible resource type '{resourceContext.PublicName}'."); - } - - if (state.Request.Kind == EndpointKind.AtomicOperations) - { - throw new DeserializationException(state.Position, "Resource type mismatch between 'ref.type' and 'data.type' element.", - $"Expected resource of type '{requirements.ResourceContext.PublicName}' in 'data.type', instead of '{resourceContext.PublicName}'."); - } - - string title = requirements.RelationshipName != null ? "Resource type is incompatible with relationship type." : - state.Request.Kind == EndpointKind.AtomicOperations ? "Resource type is incompatible with type in ref." : - "Resource type is incompatible with endpoint URL."; - - string detail = requirements.RelationshipName != null - ? $"Type '{resourceContext.PublicName}' is incompatible with type '{requirements.ResourceContext.PublicName}' of relationship '{requirements.RelationshipName}'." - : $"Type '{resourceContext.PublicName}' is incompatible with type '{requirements.ResourceContext.PublicName}'."; - - throw new JsonApiException(new ErrorObject(HttpStatusCode.Conflict) - { - Title = title, - Detail = detail, - Source = new ErrorSource - { - Pointer = state.Position.ToSourcePointer() - } - }); - } + AssertIsCompatibleResourceType(resourceContext, requirements.ResourceContext, requirements.RelationshipName, state); return resourceContext; } @@ -169,6 +136,18 @@ private static void AssertIsKnownResourceType(ResourceContext resourceContext, s } } + private static void AssertIsCompatibleResourceType(ResourceContext actual, ResourceContext expected, string relationshipName, RequestAdapterState state) + { + if (expected != null && !expected.ResourceType.IsAssignableFrom(actual.ResourceType)) + { + string message = relationshipName != null + ? $"Type '{actual.PublicName}' is incompatible with type '{expected.PublicName}' of relationship '{relationshipName}'." + : $"Type '{actual.PublicName}' is incompatible with type '{expected.PublicName}'."; + + throw new ModelConversionException(state.Position, "Incompatible resource type found.", message, HttpStatusCode.Conflict); + } + } + private static void AssertHasNoLid(IResourceIdentity identity, RequestAdapterState state) { if (identity.Lid != null) diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityRequirements.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityRequirements.cs index 2dcdfb6a23..611aa8135d 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityRequirements.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityRequirements.cs @@ -34,10 +34,5 @@ public sealed class ResourceIdentityRequirements /// When not null, indicates the name of the relationship to use in error messages. /// public string RelationshipName { get; init; } - - /// - /// This temporary property will be removed in a future commit. - /// - public bool UseLegacyError { get; init; } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index 8e712a1137..146ff96c57 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -445,14 +445,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Relationship contains incompatible resource type."); - error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index 47e6ee6238..8caf56721b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -480,14 +480,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Relationship contains incompatible resource type."); - error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index 00670b143b..03ad0931f4 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -894,14 +894,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Relationship contains incompatible resource type."); - error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index 7e2418ceef..5d9b15af84 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -857,14 +857,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Relationship contains incompatible resource type."); - error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index 1183bbd0ea..f66a82c980 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -1003,14 +1003,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Relationship contains incompatible resource type."); - error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 90f9880a03..80f00e3c47 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -1213,14 +1213,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Relationship contains incompatible resource type."); - error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 597769ddd4..6f0fbfe0fb 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -670,14 +670,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Relationship contains incompatible resource type."); - error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 9a513d67ff..3603744066 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -1085,14 +1085,14 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: 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'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index 8ca6ffa547..6792d81d45 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -916,14 +916,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Relationship contains incompatible resource type."); - error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 5d82452018..69c9f31a85 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -667,7 +667,7 @@ public async Task Cannot_create_on_resource_type_mismatch_between_url_and_body() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); - error.Title.Should().Be("Resource type is incompatible with endpoint URL."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'workItems'."); error.Source.Pointer.Should().Be("/data/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index c5c3c76a04..33a82751c3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -540,14 +540,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Relationship contains incompatible resource type."); - error.Detail.Should().Be("Relationship 'subscribers' contains incompatible resource type 'rgbColors'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'subscribers'."); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index 20341b529d..54ed0b7eaf 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -478,14 +478,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Relationship contains incompatible resource type."); - error.Detail.Should().Be("Relationship 'assignee' contains incompatible resource type 'rgbColors'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'assignee'."); error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index 4ef2be3689..0b5cd08f17 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -598,7 +598,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); - error.Title.Should().Be("Resource type is incompatible with relationship type."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'userAccounts' is incompatible with type 'workTags' of relationship 'tags'."); error.Source.Pointer.Should().Be("/data[0]/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index d1f2b3b07a..dca4399e53 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -714,7 +714,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); - error.Title.Should().Be("Resource type is incompatible with relationship type."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'userAccounts' is incompatible with type 'workTags' of relationship 'tags'."); error.Source.Pointer.Should().Be("/data[0]/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index 756202c8d7..d471beda96 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -626,7 +626,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); - error.Title.Should().Be("Resource type is incompatible with relationship type."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'userAccounts' is incompatible with type 'workTags' of relationship 'tags'."); error.Source.Pointer.Should().Be("/data[0]/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index d6ad4b0af7..5bbf0e77f6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -575,7 +575,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); - error.Title.Should().Be("Resource type is incompatible with relationship type."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'assignee'."); error.Source.Pointer.Should().Be("/data/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index 488e781d4c..b95a150e44 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -689,14 +689,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Relationship contains incompatible resource type."); - error.Detail.Should().Be("Relationship 'subscribers' contains incompatible resource type 'rgbColors'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'subscribers'."); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 46dd9be649..5e840f18c3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -960,7 +960,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); - error.Title.Should().Be("Resource type is incompatible with endpoint URL."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'workItems'."); error.Source.Pointer.Should().Be("/data/type"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index ffdeba0f47..a996632710 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -676,14 +676,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Relationship contains incompatible resource type."); - error.Detail.Should().Be("Relationship 'assignee' contains incompatible resource type 'rgbColors'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'assignee'."); error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); From d4549e84d87c7153fa5688fe0c7ed836482d6867 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 29 Sep 2021 18:14:43 +0200 Subject: [PATCH 12/49] Revert the use of different exception, because this way the request body does not get added to the error response meta --- .../ForbiddenClientGeneratedIdException.cs | 25 ------------------- .../ResourceIdentityAdapter.cs | 2 +- .../Creating/AtomicCreateResourceTests.cs | 4 ++- .../ReadWrite/Creating/CreateResourceTests.cs | 4 ++- 4 files changed, 7 insertions(+), 28 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Errors/ForbiddenClientGeneratedIdException.cs diff --git a/src/JsonApiDotNetCore/Errors/ForbiddenClientGeneratedIdException.cs b/src/JsonApiDotNetCore/Errors/ForbiddenClientGeneratedIdException.cs deleted file mode 100644 index 29cd67df78..0000000000 --- a/src/JsonApiDotNetCore/Errors/ForbiddenClientGeneratedIdException.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Net; -using JetBrains.Annotations; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Errors -{ - /// - /// The error that is thrown when creating a resource with a client-generated ID. - /// - [PublicAPI] - public sealed class ForbiddenClientGeneratedIdException : JsonApiException - { - public ForbiddenClientGeneratedIdException(string sourcePointer) - : base(new ErrorObject(HttpStatusCode.Forbidden) - { - Title = "The use of client-generated IDs is disabled.", - Source = new ErrorSource - { - Pointer = sourcePointer - } - }) - { - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs index 5ab895b21e..2df546d9a1 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs @@ -182,7 +182,7 @@ private static void AssertHasNoId(IResourceIdentity identity, RequestAdapterStat if (identity.Id != null) { using IDisposable _ = state.Position.PushElement("id"); - throw new ForbiddenClientGeneratedIdException(state.Position.ToSourcePointer()); + throw new ModelConversionException(state.Position, "The use of client-generated IDs is disabled.", null, HttpStatusCode.Forbidden); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index 9acb9abe56..1d220b3352 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -459,9 +459,11 @@ public async Task Cannot_create_resource_with_client_generated_ID() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("The use of client-generated IDs is disabled."); + error.Title.Should().Be("Failed to deserialize request body: The use of client-generated IDs is disabled."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 69c9f31a85..90eea49633 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -456,9 +456,11 @@ public async Task Cannot_create_resource_with_client_generated_ID() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("The use of client-generated IDs is disabled."); + error.Title.Should().Be("Failed to deserialize request body: The use of client-generated IDs is disabled."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/id"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] From c2e31d322fc4fd5e95fabe84816990d06b2773a9 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 29 Sep 2021 22:24:15 +0200 Subject: [PATCH 13/49] Unified error messages about mismatches in 'id' and 'lid' values --- .../Errors/ResourceIdMismatchException.cs | 22 --- .../RelationshipDataAdapter.cs | 1 - .../ResourceIdentityAdapter.cs | 145 ++++++++---------- .../Resources/AtomicUpdateResourceTests.cs | 28 ++-- .../Updating/Resources/UpdateResourceTests.cs | 4 +- 5 files changed, 84 insertions(+), 116 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs diff --git a/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs deleted file mode 100644 index fdfd6b6fe9..0000000000 --- a/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Net; -using JetBrains.Annotations; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Errors -{ - /// - /// The error that is thrown when the resource ID in the request body does not match the ID in the current endpoint URL. - /// - [PublicAPI] - public sealed class ResourceIdMismatchException : JsonApiException - { - public ResourceIdMismatchException(string bodyId, string endpointId, string requestPath) - : 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}'." - }) - { - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs index 73b484e2e7..54c99424c5 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs index 2df546d9a1..9e6c52b16b 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs @@ -1,7 +1,6 @@ using System; using System.Net; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -34,75 +33,7 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory ArgumentGuard.NotNull(state, nameof(state)); ResourceContext resourceContext = ConvertType(identity, requirements, state); - - if (state.Request.Kind != EndpointKind.AtomicOperations) - { - AssertHasNoLid(identity, state); - } - - AssertNoIdWithLid(identity, state); - - if (requirements.IdConstraint == JsonElementConstraint.Required) - { - AssertHasIdOrLid(identity, state); - } - else if (requirements.IdConstraint == JsonElementConstraint.Forbidden) - { - AssertHasNoId(identity, state); - } - - if (requirements.IdValue != null && identity.Id != null && identity.Id != requirements.IdValue) - { - using (state.Position.PushElement("id")) - { - if (state.Request.Kind == EndpointKind.AtomicOperations) - { - throw new DeserializationException(state.Position, "Resource ID mismatch between 'ref.id' and 'data.id' element.", - $"Expected resource with ID '{requirements.IdValue}' in 'data.id', instead of '{identity.Id}'."); - } - - throw new JsonApiException(new ErrorObject(HttpStatusCode.Conflict) - { - Title = "Resource ID mismatch between request body and endpoint URL.", - Detail = $"Expected resource ID '{requirements.IdValue}', instead of '{identity.Id}'.", - Source = new ErrorSource - { - Pointer = state.Position.ToSourcePointer() - } - }); - } - } - - if (requirements.LidValue != null && identity.Lid != null && identity.Lid != requirements.LidValue) - { - using (state.Position.PushElement("lid")) - { - throw new DeserializationException(state.Position, "Resource local ID mismatch between 'ref.lid' and 'data.lid' element.", - $"Expected resource with local ID '{requirements.LidValue}' in 'data.lid', instead of '{identity.Lid}'."); - } - } - - if (requirements.IdValue != null && identity.Lid != null) - { - using (state.Position.PushElement("lid")) - { - throw new DeserializationException(state.Position, "Resource identity mismatch between 'ref.id' and 'data.lid' element.", - $"Expected resource with ID '{requirements.IdValue}' in 'data.id', instead of '{identity.Lid}' in 'data.lid'."); - } - } - - if (requirements.LidValue != null && identity.Id != null) - { - using (state.Position.PushElement("id")) - { - throw new DeserializationException(state.Position, "Resource identity mismatch between 'ref.lid' and 'data.id' element.", - $"Expected resource with local ID '{requirements.LidValue}' in 'data.lid', instead of '{identity.Id}' in 'data.id'."); - } - } - - IIdentifiable resource = _resourceFactory.CreateInstance(resourceContext.ResourceType); - AssignStringId(identity, resource, state); - resource.LocalId = identity.Lid; + IIdentifiable resource = CreateResource(identity, requirements, resourceContext.ResourceType, state); return (resource, resourceContext); } @@ -148,6 +79,34 @@ private static void AssertIsCompatibleResourceType(ResourceContext actual, Resou } } + private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentityRequirements requirements, Type resourceType, + RequestAdapterState state) + { + if (state.Request.Kind != EndpointKind.AtomicOperations) + { + AssertHasNoLid(identity, state); + } + + AssertNoIdWithLid(identity, state); + + if (requirements.IdConstraint == JsonElementConstraint.Required) + { + AssertHasIdOrLid(identity, requirements, state); + } + else if (requirements.IdConstraint == JsonElementConstraint.Forbidden) + { + AssertHasNoId(identity, state); + } + + AssertSameIdValue(identity, requirements.IdValue, state); + AssertSameLidValue(identity, requirements.LidValue, state); + + IIdentifiable resource = _resourceFactory.CreateInstance(resourceType); + AssignStringId(identity, resource, state); + resource.LocalId = identity.Lid; + return resource; + } + private static void AssertHasNoLid(IResourceIdentity identity, RequestAdapterState state) { if (identity.Lid != null) @@ -165,14 +124,25 @@ private static void AssertNoIdWithLid(IResourceIdentity identity, RequestAdapter } } - private static void AssertHasIdOrLid(IResourceIdentity identity, RequestAdapterState state) + private static void AssertHasIdOrLid(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) { - if (identity.Id == null && identity.Lid == null) + string message = null; + + if (requirements.IdValue != null && identity.Id == null) + { + message = "The 'id' element is required."; + } + else if (requirements.LidValue != null && identity.Lid == null) { - string message = state.Request.Kind == EndpointKind.AtomicOperations - ? "The 'id' or 'lid' element is required." - : "The 'id' element is required."; + message = "The 'lid' element is required."; + } + else if (identity.Id == null && identity.Lid == null) + { + message = state.Request.Kind == EndpointKind.AtomicOperations ? "The 'id' or 'lid' element is required." : "The 'id' element is required."; + } + if (message != null) + { throw new ModelConversionException(state.Position, message, null); } } @@ -186,18 +156,39 @@ private static void AssertHasNoId(IResourceIdentity identity, RequestAdapterStat } } - private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state) + private static void AssertSameIdValue(IResourceIdentity identity, string expected, RequestAdapterState state) { - if (identity.Id != null) + if (expected != null && identity.Id != expected) { using IDisposable _ = state.Position.PushElement("id"); + throw new ModelConversionException(state.Position, "Conflicting 'id' values found.", $"Expected '{expected}' instead of '{identity.Id}'.", + HttpStatusCode.Conflict); + } + } + + private static void AssertSameLidValue(IResourceIdentity identity, string expected, RequestAdapterState state) + { + if (expected != null && identity.Lid != expected) + { + using IDisposable _ = state.Position.PushElement("lid"); + + throw new ModelConversionException(state.Position, "Conflicting 'lid' values found.", $"Expected '{expected}' instead of '{identity.Lid}'.", + HttpStatusCode.Conflict); + } + } + + private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state) + { + if (identity.Id != null) + { try { resource.StringId = identity.Id; } catch (FormatException exception) { + using IDisposable _ = state.Position.PushElement("id"); throw new DeserializationException(state.Position, null, exception.Message); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 3603744066..7200e0d9b8 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -1136,14 +1136,14 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Resource ID mismatch between 'ref.id' and 'data.id' element."); - error.Detail.Should().Be($"Expected resource with ID '{performerId1}' in 'data.id', instead of '{performerId2}'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Conflicting 'id' values found."); + error.Detail.Should().Be($"Expected '{performerId1}' instead of '{performerId2}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); } @@ -1184,14 +1184,14 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: 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'."); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Conflicting 'lid' values found."); + error.Detail.Should().Be("Expected 'local-1' instead of 'local-2'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/lid"); } @@ -1240,9 +1240,9 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da 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 '{performerId}' in 'data.id', instead of 'local-1' in 'data.lid'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]/data/lid"); + error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] @@ -1290,9 +1290,9 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da 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 '{performerId}' in 'data.id'."); - error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); + error.Title.Should().Be("Failed to deserialize request body: The 'lid' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 5e840f18c3..4a76ad472d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -998,8 +998,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); - error.Title.Should().Be("Resource ID mismatch between request body and endpoint URL."); - error.Detail.Should().Be($"Expected resource ID '{existingWorkItems[1].StringId}', instead of '{existingWorkItems[0].StringId}'."); + error.Title.Should().Be("Failed to deserialize request body: Conflicting 'id' values found."); + error.Detail.Should().Be($"Expected '{existingWorkItems[1].StringId}' instead of '{existingWorkItems[0].StringId}'."); error.Source.Pointer.Should().Be("/data/id"); } From 5ea961f07ed06ce7d8457637e1ce4eca68848bf4 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 29 Sep 2021 23:08:10 +0200 Subject: [PATCH 14/49] Additional unification of error messages --- .../AtomicOperationObjectAdapter.cs | 6 +++--- .../RequestAdapters/AtomicReferenceAdapter.cs | 13 ------------ .../OperationsDocumentAdapter.cs | 7 ++++--- .../RelationshipDataAdapter.cs | 6 +++--- .../ResourceDocumentAdapter.cs | 20 +------------------ .../ResourceIdentityAdapter.cs | 16 +++++++++++++++ .../Creating/AtomicCreateResourceTests.cs | 4 ++-- .../Deleting/AtomicDeleteResourceTests.cs | 2 +- .../Mixed/MaximumOperationsPerRequestTests.cs | 4 ++-- .../AtomicAddToToManyRelationshipTests.cs | 10 +++++----- ...AtomicRemoveFromToManyRelationshipTests.cs | 8 ++++---- .../AtomicReplaceToManyRelationshipTests.cs | 2 +- .../AtomicUpdateToOneRelationshipTests.cs | 2 +- .../Resources/AtomicUpdateResourceTests.cs | 2 +- .../AddToToManyRelationshipTests.cs | 4 ++-- .../RemoveFromToManyRelationshipTests.cs | 4 ++-- 16 files changed, 48 insertions(+), 62 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs index 63c258be28..947af95784 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs @@ -63,7 +63,7 @@ private static void AssertNoHref(AtomicOperationObject atomicOperationObject, Re { using (state.Position.PushElement("href")) { - throw new DeserializationException(state.Position, "Usage of the 'href' element is not supported.", null); + throw new ModelConversionException(state.Position, "The 'href' element is not supported.", null); } } } @@ -78,7 +78,7 @@ private WriteOperationKind ConvertOperationCode(AtomicOperationObject atomicOper { using (state.Position.PushElement("ref")) { - throw new DeserializationException(state.Position, "The 'ref.relationship' element is required.", null); + throw new ModelConversionException(state.Position, "The 'relationship' element is required.", null); } } @@ -92,7 +92,7 @@ private WriteOperationKind ConvertOperationCode(AtomicOperationObject atomicOper { if (atomicOperationObject.Ref == null) { - throw new DeserializationException(state.Position, "The 'ref' element is required.", null); + throw new ModelConversionException(state.Position, "The 'ref' element is required.", null); } return atomicOperationObject.Ref.Relationship != null ? WriteOperationKind.RemoveFromRelationship : WriteOperationKind.DeleteResource; diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs index 5da4500b24..abd78b336f 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs @@ -1,6 +1,5 @@ using System; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; @@ -44,17 +43,5 @@ private RelationshipAttribute ConvertRelationship(string relationshipName, Resou return relationship; } - - private static void AssertToManyInAddOrRemoveRelationship(RelationshipAttribute relationship, RequestAdapterState state) - { - bool requireToManyRelationship = state.Request.WriteOperation == WriteOperationKind.AddToRelationship || - state.Request.WriteOperation == WriteOperationKind.RemoveFromRelationship; - - if (requireToManyRelationship && relationship is not HasManyAttribute) - { - throw new DeserializationException(state.Position, "Only to-many relationships can be targeted through this operation.", - $"Relationship '{relationship.PublicName}' must be a to-many relationship."); - } - } } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs index 60964f39da..3d44c4f117 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs @@ -38,7 +38,7 @@ private static void AssertHasOperations(IEnumerable atomi { if (atomicOperationObjects.IsNullOrEmpty()) { - throw new DeserializationException(state.Position, "No operations found.", null); + throw new ModelConversionException(state.Position, "No operations found.", null); } } @@ -46,8 +46,9 @@ private void AssertMaxOperationsNotExceeded(ICollection a { if (atomicOperationObjects.Count > _options.MaximumOperationsPerRequest) { - throw new DeserializationException(state.Position, "Request exceeds the maximum number of operations.", - $"The number of operations in this request ({atomicOperationObjects.Count}) is higher than {_options.MaximumOperationsPerRequest}."); + throw new ModelConversionException(state.Position, "Too many operations in request.", + $"The number of operations in this request ({atomicOperationObjects.Count}) is higher " + + $"than the maximum of {_options.MaximumOperationsPerRequest}."); } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs index 54c99424c5..1db4c350ea 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs @@ -90,14 +90,14 @@ public object Convert(SingleOrManyData data, Relations private IIdentifiable ConvertToOneRelationshipData(SingleOrManyData data, RelationshipAttribute relationship, ResourceIdentityRequirements requirements, RequestAdapterState state) { - AssertHasNoManyValue(data, relationship, state); + AssertHasSingleValue(data, relationship, state); return data.SingleValue != null ? _resourceIdentifierObjectAdapter.Convert(data.SingleValue, requirements, state) : null; } - private static void AssertHasNoManyValue(SingleOrManyData data, RelationshipAttribute relationship, RequestAdapterState state) + private static void AssertHasSingleValue(SingleOrManyData data, RelationshipAttribute relationship, RequestAdapterState state) { - if (data.ManyValue != null) + if (!data.IsAssigned || data.ManyValue != null) { throw new DeserializationException(state.Position, "Expected single data element for to-one relationship.", $"Expected single data element for '{relationship.PublicName}' relationship."); diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDocumentAdapter.cs index 724b1c0834..3b9043e176 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDocumentAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDocumentAdapter.cs @@ -1,10 +1,7 @@ using System.Collections.Generic; -using System.Net; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Serialization.RequestAdapters @@ -50,7 +47,7 @@ public object Convert(Document document, RequestAdapterState state) return new HashSet(IdentifiableComparer.Instance); } - AssertToManyInAddOrRemoveRelationship(state); + ResourceIdentityAdapter.AssertToManyInAddOrRemoveRelationship(state.Request.Relationship, state); state.WritableTargetedFields.Relationships.Add(state.Request.Relationship); return _relationshipDataAdapter.Convert(document.Data, state.Request.Relationship, false, state); @@ -75,20 +72,5 @@ private ResourceIdentityRequirements CreateIdentityRequirements(RequestAdapterSt return requirements; } - - private static void AssertToManyInAddOrRemoveRelationship(RequestAdapterState state) - { - bool requireToManyRelationship = state.Request.WriteOperation == WriteOperationKind.AddToRelationship || - state.Request.WriteOperation == WriteOperationKind.RemoveFromRelationship; - - if (requireToManyRelationship && state.Request.Relationship is not HasManyAttribute) - { - throw new JsonApiException(new ErrorObject(HttpStatusCode.Forbidden) - { - Title = "Only to-many relationships can be targeted through this endpoint.", - Detail = $"Relationship '{state.Request.Relationship.PublicName}' must be a to-many relationship." - }); - } - } } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs index 9e6c52b16b..5dc2476821 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs @@ -203,5 +203,21 @@ protected static void AssertIsKnownRelationship(RelationshipAttribute relationsh $"Relationship '{relationshipName}' does not exist on resource type '{resourceContext.PublicName}'."); } } + + protected internal static void AssertToManyInAddOrRemoveRelationship(RelationshipAttribute relationship, RequestAdapterState state) + { + bool requireToManyRelationship = state.Request.WriteOperation == WriteOperationKind.AddToRelationship || + state.Request.WriteOperation == WriteOperationKind.RemoveFromRelationship; + + if (requireToManyRelationship && relationship is not HasManyAttribute) + { + string message = state.Request.Kind == EndpointKind.AtomicOperations + ? "Only to-many relationships can be targeted through this operation." + : "Only to-many relationships can be targeted through this endpoint."; + + throw new ModelConversionException(state.Position, message, $"Relationship '{relationship.PublicName}' is not a to-many relationship.", + HttpStatusCode.Forbidden); + } + } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index 1d220b3352..3973f6b6d6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -494,7 +494,7 @@ public async Task Cannot_create_resource_for_href_element() 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.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } @@ -530,7 +530,7 @@ public async Task Cannot_create_resource_for_ref_element() 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.Title.Should().Be("Failed to deserialize request body: The 'relationship' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index 9a4b70553e..5b34f704b0 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -356,7 +356,7 @@ public async Task Cannot_delete_resource_for_href_element() 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.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs index ae7fc2f24f..468d6dea73 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs @@ -72,8 +72,8 @@ public async Task Cannot_process_more_operations_than_maximum() 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.Title.Should().Be("Failed to deserialize request body: Too many operations in request."); + error.Detail.Should().Be("The number of operations in this request (3) is higher than the maximum of 2."); error.Source.Pointer.Should().Be("/atomic:operations"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index 03ad0931f4..453b5c2cdf 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -64,14 +64,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this operation."); - error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); + error.Detail.Should().Be("Relationship 'ownedBy' is not a to-many relationship."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); } @@ -263,7 +263,7 @@ public async Task Cannot_add_for_href_element() 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.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } @@ -507,7 +507,7 @@ public async Task Cannot_add_for_missing_relationship_in_ref() 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.Title.Should().Be("Failed to deserialize request body: The 'relationship' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index 5d9b15af84..fdd3ac13e5 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -65,14 +65,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this operation."); - error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); + error.Detail.Should().Be("Relationship 'ownedBy' is not a to-many relationship."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); } @@ -263,7 +263,7 @@ public async Task Cannot_remove_for_href_element() 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.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index f66a82c980..f7b9c47e62 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -299,7 +299,7 @@ public async Task Cannot_replace_for_href_element() 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.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 80f00e3c47..cb3b5057a3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -546,7 +546,7 @@ public async Task Cannot_create_for_href_element() 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.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 7200e0d9b8..128a27934d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -631,7 +631,7 @@ public async Task Cannot_update_resource_for_href_element() 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.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index 0b5cd08f17..707aa30f3f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -58,8 +58,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("Only to-many relationships can be targeted through this endpoint."); - error.Detail.Should().Be("Relationship 'assignee' must be a to-many relationship."); + error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this endpoint."); + error.Detail.Should().Be("Relationship 'assignee' is not a to-many relationship."); error.Source.Should().BeNull(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index dca4399e53..52b814480c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -73,8 +73,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("Only to-many relationships can be targeted through this endpoint."); - error.Detail.Should().Be("Relationship 'assignee' must be a to-many relationship."); + error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this endpoint."); + error.Detail.Should().Be("Relationship 'assignee' is not a to-many relationship."); error.Source.Should().BeNull(); } From 6244ef711d993a3d9cf34ba939e13e6981a8cf2b Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 29 Sep 2021 23:19:14 +0200 Subject: [PATCH 15/49] Unified error messages for failed type conversion --- .../Serialization/RequestAdapters/ResourceIdentityAdapter.cs | 2 +- .../Serialization/RequestAdapters/ResourceObjectAdapter.cs | 2 +- .../AtomicOperations/Creating/AtomicCreateResourceTests.cs | 2 +- .../AtomicCreateResourceWithClientGeneratedIdTests.cs | 2 +- .../AtomicOperations/Deleting/AtomicDeleteResourceTests.cs | 2 +- .../Relationships/AtomicReplaceToManyRelationshipTests.cs | 4 ++-- .../Relationships/AtomicUpdateToOneRelationshipTests.cs | 4 ++-- .../Updating/Resources/AtomicUpdateResourceTests.cs | 4 ++-- .../ReadWrite/Creating/CreateResourceTests.cs | 2 +- .../ReadWrite/Updating/Resources/UpdateResourceTests.cs | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs index 5dc2476821..2b66a51394 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs @@ -189,7 +189,7 @@ private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, catch (FormatException exception) { using IDisposable _ = state.Position.PushElement("id"); - throw new DeserializationException(state.Position, null, exception.Message); + throw new ModelConversionException(state.Position, "Incompatible 'id' value found.", exception.Message); } } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs index d501972114..a0947bf2e6 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs @@ -96,7 +96,7 @@ private static void AssertNoInvalidAttribute(object attributeValue, RequestAdapt string typeName = info.AttributeType.GetFriendlyTypeName(); - throw new DeserializationException(state.Position, null, + throw new ModelConversionException(state.Position, "Incompatible attribute value found.", $"Failed to convert attribute '{info.AttributeName}' with value '{info.JsonValue}' of type '{info.JsonType}' to type '{typeName}'."); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index 3973f6b6d6..272191a799 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -804,7 +804,7 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible attribute value found."); 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]/data/attributes/bornAt"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs index 21788310ba..910597d5e9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs @@ -225,7 +225,7 @@ public async Task Cannot_create_resource_for_incompatible_ID() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index 5b34f704b0..d12f3a461f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -575,7 +575,7 @@ public async Task Cannot_delete_resource_for_incompatible_ID() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int64'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index f7b9c47e62..cc514e1601 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -523,7 +523,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int16'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); } @@ -955,7 +955,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/id"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index cb3b5057a3..82c8ed4dfc 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -762,7 +762,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); } @@ -1168,7 +1168,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 128a27934d..cdae498e72 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -1425,7 +1425,7 @@ public async Task Cannot_update_resource_for_incompatible_ID() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); } @@ -1621,7 +1621,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible attribute value found."); 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]/data/attributes/bornAt"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 90eea49633..9f4c536131 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -797,7 +797,7 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible attribute value found."); error.Detail.Should().Be("Failed to convert attribute 'dueAt' with value 'not-a-valid-time' of type 'String' to type 'Nullable'."); error.Source.Pointer.Should().Be("/data/attributes/dueAt"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 4a76ad472d..1b33f36a69 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -1252,7 +1252,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); + error.Title.Should().Be("Failed to deserialize request body: Incompatible attribute value found."); error.Detail.Should().Match("Failed to convert attribute 'dueAt' with value '*start*end*' of type 'Object' to type 'Nullable'."); error.Source.Pointer.Should().Be("/data/attributes/dueAt"); From 72f02b087d1496c6ba497a41d03bca59c42a110c Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 30 Sep 2021 14:01:59 +0200 Subject: [PATCH 16/49] Fix cibuild --- .../Serialization/RequestAdapters/ResourceObjectAdapter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs index a0947bf2e6..fd6dcf5d3c 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs @@ -71,7 +71,7 @@ private void ConvertAttribute(IIdentifiable resource, string attributeName, obje AssertNoBlockedChange(attr, state); AssertNotReadOnly(attr, state); - attr.SetValue(resource, attributeValue); + attr!.SetValue(resource, attributeValue); state.WritableTargetedFields.Attributes.Add(attr); } @@ -154,7 +154,7 @@ private void ConvertRelationship(string relationshipName, SingleOrManyData Date: Thu, 30 Sep 2021 15:06:09 +0200 Subject: [PATCH 17/49] Unified error messages about data presense and its value: null/object/array --- .../ToManyRelationshipRequiredException.cs | 22 --- .../AtomicOperationObjectAdapter.cs | 12 +- .../RequestAdapters/AtomicReferenceAdapter.cs | 2 - .../RequestAdapters/BaseDataAdapter.cs | 55 +++++++ .../OperationsDocumentAdapter.cs | 20 ++- .../RelationshipDataAdapter.cs | 34 +--- .../RequestAdapters/ResourceDataAdapter.cs | 38 ++--- .../RequestAdapters/ResourceObjectAdapter.cs | 2 - .../Creating/AtomicCreateResourceTests.cs | 81 +++++++--- ...eateResourceWithToManyRelationshipTests.cs | 58 ++++++- ...reateResourceWithToOneRelationshipTests.cs | 142 ++++++++++------ .../AtomicAddToToManyRelationshipTests.cs | 99 +++++++++++- ...AtomicRemoveFromToManyRelationshipTests.cs | 99 +++++++++++- .../AtomicReplaceToManyRelationshipTests.cs | 99 +++++++++++- .../AtomicUpdateToOneRelationshipTests.cs | 52 +++++- .../AtomicReplaceToManyRelationshipTests.cs | 111 ++++++++++++- .../Resources/AtomicUpdateResourceTests.cs | 99 ++++++++---- .../AtomicUpdateToOneRelationshipTests.cs | 57 ++++++- .../ReadWrite/Creating/CreateResourceTests.cs | 32 +++- ...eateResourceWithToManyRelationshipTests.cs | 53 +++++- ...reateResourceWithToOneRelationshipTests.cs | 151 ++++++++++++------ .../AddToToManyRelationshipTests.cs | 51 +++++- .../RemoveFromToManyRelationshipTests.cs | 51 +++++- .../ReplaceToManyRelationshipTests.cs | 51 +++++- .../UpdateToOneRelationshipTests.cs | 124 +++++++++----- .../ReplaceToManyRelationshipTests.cs | 62 ++++++- .../Updating/Resources/UpdateResourceTests.cs | 40 ++++- .../Resources/UpdateToOneRelationshipTests.cs | 145 +++++++++++------ 28 files changed, 1430 insertions(+), 412 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Errors/ToManyRelationshipRequiredException.cs create mode 100644 src/JsonApiDotNetCore/Serialization/RequestAdapters/BaseDataAdapter.cs diff --git a/src/JsonApiDotNetCore/Errors/ToManyRelationshipRequiredException.cs b/src/JsonApiDotNetCore/Errors/ToManyRelationshipRequiredException.cs deleted file mode 100644 index c5b100904f..0000000000 --- a/src/JsonApiDotNetCore/Errors/ToManyRelationshipRequiredException.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Net; -using JetBrains.Annotations; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Errors -{ - /// - /// The error that is thrown when an attempt is made to update a to-one relationship from a to-many relationship endpoint. - /// - [PublicAPI] - public sealed class ToManyRelationshipRequiredException : JsonApiException - { - public ToManyRelationshipRequiredException(string relationshipName) - : base(new ErrorObject(HttpStatusCode.Forbidden) - { - Title = "Only to-many relationships can be updated through this endpoint.", - Detail = $"Relationship '{relationshipName}' must be a to-many relationship." - }) - { - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs index 947af95784..92b20ae1ed 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs @@ -61,10 +61,8 @@ private static void AssertNoHref(AtomicOperationObject atomicOperationObject, Re { if (atomicOperationObject.Href != null) { - using (state.Position.PushElement("href")) - { - throw new ModelConversionException(state.Position, "The 'href' element is not supported.", null); - } + using IDisposable _ = state.Position.PushElement("href"); + throw new ModelConversionException(state.Position, "The 'href' element is not supported.", null); } } @@ -76,10 +74,8 @@ private WriteOperationKind ConvertOperationCode(AtomicOperationObject atomicOper { if (atomicOperationObject.Ref is { Relationship: null }) { - using (state.Position.PushElement("ref")) - { - throw new ModelConversionException(state.Position, "The 'relationship' element is required.", null); - } + using IDisposable _ = state.Position.PushElement("ref"); + throw new ModelConversionException(state.Position, "The 'relationship' element is required.", null); } return atomicOperationObject.Ref == null ? WriteOperationKind.CreateResource : WriteOperationKind.AddToRelationship; diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs index abd78b336f..95ff4f56e6 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs @@ -22,7 +22,6 @@ public AtomicReferenceResult Convert(AtomicReference atomicReference, ResourceId ArgumentGuard.NotNull(state, nameof(state)); using IDisposable _ = state.Position.PushElement("ref"); - (IIdentifiable resource, ResourceContext resourceContext) = ConvertResourceIdentity(atomicReference, requirements, state); RelationshipAttribute relationship = atomicReference.Relationship != null @@ -35,7 +34,6 @@ public AtomicReferenceResult Convert(AtomicReference atomicReference, ResourceId private RelationshipAttribute ConvertRelationship(string relationshipName, ResourceContext resourceContext, RequestAdapterState state) { using IDisposable _ = state.Position.PushElement("relationship"); - RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(relationshipName); AssertIsKnownRelationship(relationship, relationshipName, resourceContext, state); diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/BaseDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/BaseDataAdapter.cs new file mode 100644 index 0000000000..fa22de3d32 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/BaseDataAdapter.cs @@ -0,0 +1,55 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.RequestAdapters +{ + /// + /// Contains shared assertions for derived types. + /// + public abstract class BaseDataAdapter + { + [AssertionMethod] + protected static void AssertHasData(SingleOrManyData data, RequestAdapterState state) + where T : class, IResourceIdentity + { + if (!data.IsAssigned) + { + throw new ModelConversionException(state.Position, "The 'data' element is required.", null); + } + } + + [AssertionMethod] + protected static void AssertHasSingleValue(SingleOrManyData data, bool allowNull, RequestAdapterState state) + where T : class, IResourceIdentity + { + if (data.SingleValue == null) + { + if (!allowNull) + { + throw new ModelConversionException(state.Position, + data.ManyValue == null + ? "Expected an object in 'data' element, instead of 'null'." + : "Expected an object in 'data' element, instead of an array.", null); + } + + if (data.ManyValue != null) + { + throw new ModelConversionException(state.Position, "Expected an object or 'null' in 'data' element, instead of an array.", null); + } + } + } + + [AssertionMethod] + protected static void AssertHasManyValue(SingleOrManyData data, RequestAdapterState state) + where T : class, IResourceIdentity + { + if (data.ManyValue == null) + { + throw new ModelConversionException(state.Position, + data.SingleValue == null + ? "Expected an array in 'data' element, instead of 'null'." + : "Expected an array in 'data' element, instead of an object.", null); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs index 3d44c4f117..51a7d3ba90 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; @@ -26,12 +27,10 @@ public IList Convert(Document document, RequestAdapterState ArgumentGuard.NotNull(state, nameof(state)); AssertHasOperations(document.Operations, state); - using (state.Position.PushElement("atomic:operations")) - { - AssertMaxOperationsNotExceeded(document.Operations, state); + using IDisposable _ = state.Position.PushElement("atomic:operations"); + AssertMaxOperationsNotExceeded(document.Operations, state); - return ConvertOperations(document.Operations, state); - } + return ConvertOperations(document.Operations, state); } private static void AssertHasOperations(IEnumerable atomicOperationObjects, RequestAdapterState state) @@ -59,13 +58,12 @@ private IList ConvertOperations(IEnumerable - public sealed class RelationshipDataAdapter : IRelationshipDataAdapter + /// + public sealed class RelationshipDataAdapter : BaseDataAdapter, IRelationshipDataAdapter { private static readonly CollectionConverter CollectionConverter = new(); @@ -70,9 +70,9 @@ public object Convert(SingleOrManyData data, Relations { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(state, nameof(state)); + AssertHasData(data, state); using IDisposable _ = state.Position.PushElement("data"); - ResourceContext rightResourceContext = _resourceGraph.GetResourceContext(relationship.RightType); var requirements = new ResourceIdentityRequirements @@ -83,31 +83,22 @@ public object Convert(SingleOrManyData data, Relations }; return relationship is HasOneAttribute - ? ConvertToOneRelationshipData(data, relationship, requirements, state) + ? ConvertToOneRelationshipData(data, requirements, state) : ConvertToManyRelationshipData(data, relationship, requirements, useToManyElementType, state); } - private IIdentifiable ConvertToOneRelationshipData(SingleOrManyData data, RelationshipAttribute relationship, - ResourceIdentityRequirements requirements, RequestAdapterState state) + private IIdentifiable ConvertToOneRelationshipData(SingleOrManyData data, ResourceIdentityRequirements requirements, + RequestAdapterState state) { - AssertHasSingleValue(data, relationship, state); + AssertHasSingleValue(data, true, state); return data.SingleValue != null ? _resourceIdentifierObjectAdapter.Convert(data.SingleValue, requirements, state) : null; } - private static void AssertHasSingleValue(SingleOrManyData data, RelationshipAttribute relationship, RequestAdapterState state) - { - if (!data.IsAssigned || data.ManyValue != null) - { - throw new DeserializationException(state.Position, "Expected single data element for to-one relationship.", - $"Expected single data element for '{relationship.PublicName}' relationship."); - } - } - private IEnumerable ConvertToManyRelationshipData(SingleOrManyData data, RelationshipAttribute relationship, ResourceIdentityRequirements requirements, bool useToManyElementType, RequestAdapterState state) { - AssertHasManyValue(data, relationship, state); + AssertHasManyValue(data, state); int arrayIndex = 0; var rightResources = new List(); @@ -131,14 +122,5 @@ private IEnumerable ConvertToManyRelationshipData(SingleOrManyData data, RelationshipAttribute relationship, RequestAdapterState state) - { - if (data.ManyValue == null) - { - throw new DeserializationException(state.Position, "Expected data[] element for to-many relationship.", - $"Expected data[] element for '{relationship.PublicName}' relationship."); - } - } } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataAdapter.cs index abb7e5bf72..150be2d4b1 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataAdapter.cs @@ -1,11 +1,12 @@ +using System; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Serialization.RequestAdapters { - /// - public class ResourceDataAdapter : IResourceDataAdapter + /// + public class ResourceDataAdapter : BaseDataAdapter, IResourceDataAdapter { private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly IResourceObjectAdapter _resourceObjectAdapter; @@ -24,36 +25,19 @@ public IIdentifiable Convert(SingleOrManyData data, ResourceIden { ArgumentGuard.NotNull(requirements, nameof(requirements)); ArgumentGuard.NotNull(state, nameof(state)); - AssertHasData(data, state); - using (state.Position.PushElement("data")) - { - AssertNoManyValue(data, state); + AssertHasData(data, state); - (IIdentifiable resource, ResourceContext _) = ConvertResourceObject(data, requirements, state); + using IDisposable _ = state.Position.PushElement("data"); + AssertHasSingleValue(data, false, state); - // Ensure that IResourceDefinition extensibility point sees the current operation, it case it injects IJsonApiRequest. - state.RefreshInjectables(); + (IIdentifiable resource, ResourceContext _) = ConvertResourceObject(data, requirements, state); - _resourceDefinitionAccessor.OnDeserialize(resource); - return resource; - } - } + // Ensure that IResourceDefinition extensibility point sees the current operation, it case it injects IJsonApiRequest. + state.RefreshInjectables(); - private static void AssertHasData(SingleOrManyData data, RequestAdapterState state) - { - if (data.Value == null) - { - throw new DeserializationException(state.Position, "The 'data' element is required.", null); - } - } - - private static void AssertNoManyValue(SingleOrManyData data, RequestAdapterState state) - { - if (data.ManyValue != null) - { - throw new DeserializationException(state.Position, "Expected 'data' object instead of array.", null); - } + _resourceDefinitionAccessor.OnDeserialize(resource); + return resource; } protected virtual (IIdentifiable resource, ResourceContext resourceContext) ConvertResourceObject(SingleOrManyData data, diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs index fd6dcf5d3c..dbc54a0489 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs @@ -57,7 +57,6 @@ private void ConvertAttribute(IIdentifiable resource, string attributeName, obje RequestAdapterState state) { using IDisposable _ = state.Position.PushElement(attributeName); - AttrAttribute attr = resourceContext.TryGetAttributeByPublicName(attributeName); if (attr == null && _options.AllowUnknownFieldsInRequestBody) @@ -142,7 +141,6 @@ private void ConvertRelationship(string relationshipName, SingleOrManyData(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: Expected an object in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + } + + [Fact] + public async Task Cannot_create_resource_for_array_data() + { + // Arrange + string newArtistName = _fakers.Performer.Generate().ArtistName; + var requestBody = new { atomic__operations = new[] @@ -578,10 +613,15 @@ public async Task Cannot_create_resource_for_missing_type() new { op = "add", - data = new + data = new[] { - attributes = new + new { + type = "performers", + attributes = new + { + artistName = newArtistName + } } } } @@ -600,13 +640,13 @@ public async Task Cannot_create_resource_for_missing_type() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] - public async Task Cannot_create_resource_for_unknown_type() + public async Task Cannot_create_resource_for_missing_type() { // Arrange var requestBody = new @@ -618,7 +658,9 @@ public async Task Cannot_create_resource_for_unknown_type() op = "add", data = new { - type = Unknown.ResourceType + attributes = new + { + } } } } @@ -636,17 +678,15 @@ public async Task Cannot_create_resource_for_unknown_type() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); - error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] - public async Task Cannot_create_resource_for_data_array() + public async Task Cannot_create_resource_for_unknown_type() { // Arrange - string newArtistName = _fakers.Performer.Generate().ArtistName; - var requestBody = new { atomic__operations = new[] @@ -654,16 +694,9 @@ public async Task Cannot_create_resource_for_data_array() new { op = "add", - data = new[] + data = new { - new - { - type = "performers", - attributes = new - { - artistName = newArtistName - } - } + type = Unknown.ResourceType } } } @@ -681,9 +714,9 @@ public async Task Cannot_create_resource_for_data_array() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Expected 'data' object instead of array."); - error.Detail.Should().BeNull(); - error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index 146ff96c57..55831bca96 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -533,7 +533,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_create_with_null_data_in_OneToMany_relationship() + public async Task Cannot_create_with_missing_data_in_OneToMany_relationship() { // Arrange var requestBody = new @@ -550,7 +550,6 @@ public async Task Cannot_create_with_null_data_in_OneToMany_relationship() { performers = new { - data = (object)null } } } @@ -570,9 +569,9 @@ public async Task Cannot_create_with_null_data_in_OneToMany_relationship() 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."); - error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data"); + error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers"); } [Fact] @@ -613,8 +612,53 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() 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."); + error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/tracks/data"); + } + + [Fact] + public async Task Cannot_create_with_object_data_in_ManyToMany_relationship() + { + // Arrange + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "add", + data = new + { + type = "playlists", + relationships = new + { + tracks = new + { + data = new + { + } + } + } + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: Expected an array in 'data' element, instead of an object."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/tracks/data"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index 8caf56721b..083b516139 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -256,6 +256,98 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); } + [Fact] + public async Task Cannot_create_for_missing_data_in_relationship() + { + // Arrange + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "add", + data = new + { + type = "musicTracks", + relationships = new + { + lyric = new + { + } + } + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric"); + } + + [Fact] + public async Task Cannot_create_for_array_data_in_relationship() + { + // Arrange + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "add", + data = new + { + type = "musicTracks", + relationships = new + { + lyric = new + { + data = new[] + { + new + { + type = "lyrics", + id = Unknown.StringId.For() + } + } + } + } + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: Expected an object or 'null' in 'data' element, instead of an array."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); + } + [Fact] public async Task Cannot_create_for_missing_relationship_type() { @@ -568,55 +660,5 @@ await _testContext.RunOnDatabaseAsync(async dbContext => trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); }); } - - [Fact] - public async Task Cannot_create_with_data_array_in_relationship() - { - // Arrange - var requestBody = new - { - atomic__operations = new[] - { - new - { - op = "add", - data = new - { - type = "musicTracks", - relationships = new - { - lyric = new - { - data = new[] - { - new - { - type = "lyrics", - id = Unknown.StringId.For() - } - } - } - } - } - } - } - }; - - const string route = "/operations"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: Expected single data element for to-one relationship."); - error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); - error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); - } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index 453b5c2cdf..8d63f24ee6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -550,6 +550,52 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); } + [Fact] + public async Task Cannot_add_for_missing_data() + { + // Arrange + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.MusicTracks.Add(existingTrack); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "add", + @ref = new + { + type = "musicTracks", + id = existingTrack.StringId, + relationship = "performers" + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); + } + [Fact] public async Task Cannot_add_for_null_data() { @@ -592,8 +638,57 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); + error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + } + + [Fact] + public async Task Cannot_add_for_object_data() + { + // Arrange + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.MusicTracks.Add(existingTrack); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "add", + @ref = new + { + type = "musicTracks", + id = existingTrack.StringId, + relationship = "performers" + }, + data = new + { + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: Expected an array in 'data' element, instead of an object."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index fdd3ac13e5..b525a03785 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -513,6 +513,52 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); } + [Fact] + public async Task Cannot_remove_for_missing_data() + { + // Arrange + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.MusicTracks.Add(existingTrack); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "remove", + @ref = new + { + type = "musicTracks", + id = existingTrack.StringId, + relationship = "performers" + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); + } + [Fact] public async Task Cannot_remove_for_null_data() { @@ -555,8 +601,57 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); + error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + } + + [Fact] + public async Task Cannot_remove_for_object_data() + { + // Arrange + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.MusicTracks.Add(existingTrack); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "remove", + @ref = new + { + type = "musicTracks", + id = existingTrack.StringId, + relationship = "performers" + }, + data = new + { + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: Expected an array in 'data' element, instead of an object."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index cc514e1601..92466e4b0f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -605,6 +605,52 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); } + [Fact] + public async Task Cannot_replace_for_missing_data() + { + // Arrange + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.MusicTracks.Add(existingTrack); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "update", + @ref = new + { + type = "musicTracks", + id = existingTrack.StringId, + relationship = "performers" + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); + } + [Fact] public async Task Cannot_replace_for_null_data() { @@ -647,8 +693,57 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); + error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + } + + [Fact] + public async Task Cannot_replace_for_object_data() + { + // Arrange + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.MusicTracks.Add(existingTrack); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "update", + @ref = new + { + type = "musicTracks", + id = existingTrack.StringId, + relationship = "performers" + }, + data = new + { + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: Expected an array in 'data' element, instead of an object."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 82c8ed4dfc..6cd0db03ae 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -845,7 +845,53 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() } [Fact] - public async Task Cannot_create_for_data_array() + public async Task Cannot_create_for_missing_data() + { + // Arrange + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.MusicTracks.Add(existingTrack); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "update", + @ref = new + { + type = "musicTracks", + id = existingTrack.StringId, + relationship = "lyric" + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); + } + + [Fact] + public async Task Cannot_create_for_array_data() { // Arrange MusicTrack existingTrack = _fakers.MusicTrack.Generate(); @@ -893,8 +939,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); + error.Title.Should().Be("Failed to deserialize request body: Expected an object or 'null' in 'data' element, instead of an array."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 6f0fbfe0fb..1ef126694c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -292,7 +292,58 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_replace_for_null_relationship_data() + public async Task Cannot_replace_for_missing_data_in_relationship() + { + // Arrange + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.MusicTracks.Add(existingTrack); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "update", + data = new + { + type = "musicTracks", + id = existingTrack.StringId, + relationships = new + { + performers = new + { + } + } + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers"); + } + + [Fact] + public async Task Cannot_replace_for_null_data_in_relationship() { // Arrange MusicTrack existingTrack = _fakers.MusicTrack.Generate(); @@ -338,8 +389,62 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); + error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data"); + } + + [Fact] + public async Task Cannot_replace_for_object_data_in_relationship() + { + // Arrange + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.MusicTracks.Add(existingTrack); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "update", + data = new + { + type = "musicTracks", + id = existingTrack.StringId, + relationships = new + { + performers = new + { + data = new + { + } + } + } + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: Expected an array in 'data' element, instead of an object."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index cdae498e72..e4a0fa1e96 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -869,7 +869,7 @@ public async Task Cannot_update_resource_for_missing_data() } [Fact] - public async Task Cannot_update_resource_for_missing_type_in_data() + public async Task Cannot_update_resource_for_null_data() { // Arrange var requestBody = new @@ -879,14 +879,57 @@ public async Task Cannot_update_resource_for_missing_type_in_data() new { op = "update", - data = new + data = (object)null + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: Expected an object in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + } + + [Fact] + public async Task Cannot_update_resource_for_array_data() + { + // Arrange + Performer existingPerformer = _fakers.Performer.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Performers.Add(existingPerformer); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "update", + data = new[] { - id = Unknown.StringId.Int32, - attributes = new - { - }, - relationships = new + new { + type = "performers", + id = existingPerformer.StringId, + attributes = new + { + artistName = existingPerformer.ArtistName + } } } } @@ -905,13 +948,13 @@ public async Task Cannot_update_resource_for_missing_type_in_data() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] - public async Task Cannot_update_resource_for_missing_ID_in_data() + public async Task Cannot_update_resource_for_missing_type_in_data() { // Arrange var requestBody = new @@ -923,7 +966,7 @@ public async Task Cannot_update_resource_for_missing_ID_in_data() op = "update", data = new { - type = "performers", + id = Unknown.StringId.Int32, attributes = new { }, @@ -947,13 +990,13 @@ public async Task Cannot_update_resource_for_missing_ID_in_data() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] - public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() + public async Task Cannot_update_resource_for_missing_ID_in_data() { // Arrange var requestBody = new @@ -966,8 +1009,6 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() data = new { type = "performers", - id = Unknown.StringId.For(), - lid = "local-1", attributes = new { }, @@ -991,23 +1032,15 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } [Fact] - public async Task Cannot_update_resource_for_data_array() + public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() { // Arrange - Performer existingPerformer = _fakers.Performer.Generate(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.Performers.Add(existingPerformer); - await dbContext.SaveChangesAsync(); - }); - var requestBody = new { atomic__operations = new[] @@ -1015,16 +1048,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { op = "update", - data = new[] + data = new { - new + type = "performers", + id = Unknown.StringId.For(), + lid = "local-1", + attributes = new + { + }, + relationships = new { - type = "performers", - id = existingPerformer.StringId, - attributes = new - { - artistName = existingPerformer.ArtistName - } } } } @@ -1043,7 +1076,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Expected 'data' object instead of array."); + error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index 6792d81d45..ad7a3018f4 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -564,7 +564,58 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_create_for_relationship_data_array() + public async Task Cannot_create_for_missing_data_in_relationship() + { + // Arrange + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.MusicTracks.Add(existingTrack); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "update", + data = new + { + type = "musicTracks", + id = existingTrack.StringId, + relationships = new + { + lyric = new + { + } + } + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(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: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric"); + } + + [Fact] + public async Task Cannot_create_for_array_data_in_relationship() { // Arrange MusicTrack existingTrack = _fakers.MusicTrack.Generate(); @@ -617,8 +668,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); + error.Title.Should().Be("Failed to deserialize request body: Expected an object or 'null' in 'data' element, instead of an array."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 9f4c536131..576788d8ab 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -518,7 +518,35 @@ public async Task Cannot_create_resource_for_missing_data() } [Fact] - public async Task Cannot_create_resource_for_data_array() + public async Task Cannot_create_resource_for_null_data() + { + // Arrange + var requestBody = new + { + data = (object)null + }; + + const string route = "/workItems"; + + // 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.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of 'null'."); + error.Source.Pointer.Should().Be("/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Cannot_create_resource_for_array_data() { // Arrange var requestBody = new @@ -544,7 +572,7 @@ public async Task Cannot_create_resource_for_data_array() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Expected 'data' object instead of array."); + error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index 33a82751c3..7750d5dcdf 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -620,7 +620,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_create_with_null_data_in_OneToMany_relationship() + public async Task Cannot_create_with_missing_data_in_OneToMany_relationship() { // Arrange var requestBody = new @@ -632,7 +632,6 @@ public async Task Cannot_create_with_null_data_in_OneToMany_relationship() { subscribers = new { - data = (object)null } } } @@ -650,9 +649,9 @@ public async Task Cannot_create_with_null_data_in_OneToMany_relationship() 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 'subscribers' relationship."); - error.Source.Pointer.Should().Be("/data/relationships/subscribers/data"); + error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data/relationships/subscribers"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -688,8 +687,48 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() 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 'tags' relationship."); + error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data/relationships/tags/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Cannot_create_with_object_data_in_ManyToMany_relationship() + { + // Arrange + var requestBody = new + { + data = new + { + type = "workItems", + relationships = new + { + tags = new + { + data = new + { + } + } + } + } + }; + + const string route = "/workItems"; + + // 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: Expected an array in 'data' element, instead of an object."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/tags/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index 54ed0b7eaf..e75b3d2b0e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -283,6 +283,104 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); } + [Fact] + public async Task Cannot_create_with_missing_data_in_relationship() + { + // Arrange + UserAccount existingUserAccount = _fakers.UserAccount.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.UserAccounts.Add(existingUserAccount); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "workItems", + relationships = new + { + assignee = new + { + } + } + } + }; + + const string route = "/workItems"; + + // 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: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data/relationships/assignee"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Cannot_create_with_array_data_in_relationship() + { + // Arrange + UserAccount existingUserAccount = _fakers.UserAccount.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.UserAccounts.Add(existingUserAccount); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "workItems", + relationships = new + { + assignee = new + { + data = new[] + { + new + { + type = "userAccounts", + id = existingUserAccount.StringId + } + } + } + } + } + }; + + const string route = "/workItems"; + + // 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: Expected an object or 'null' in 'data' element, instead of an array."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + [Fact] public async Task Cannot_create_for_missing_relationship_type() { @@ -562,59 +660,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); } - [Fact] - public async Task Cannot_create_with_data_array_in_relationship() - { - // Arrange - UserAccount existingUserAccount = _fakers.UserAccount.Generate(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.UserAccounts.Add(existingUserAccount); - await dbContext.SaveChangesAsync(); - }); - - var requestBody = new - { - data = new - { - type = "workItems", - relationships = new - { - assignee = new - { - data = new[] - { - new - { - type = "userAccounts", - id = existingUserAccount.StringId - } - } - } - } - } - }; - - const string route = "/workItems"; - - // 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: Expected single data element for to-one relationship."); - error.Detail.Should().Be("Expected single data element for 'assignee' relationship."); - error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); - } - [Fact] public async Task Cannot_create_resource_with_local_ID() { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index 707aa30f3f..625fd339bc 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -688,7 +688,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_add_with_null_data_in_OneToMany_relationship() + public async Task Cannot_add_with_missing_data_in_OneToMany_relationship() { // Arrange WorkItem existingWorkItem = _fakers.WorkItem.Generate(); @@ -701,7 +701,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = (object)null }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; @@ -716,9 +715,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'subscribers' relationship."); - error.Source.Pointer.Should().Be("/data"); + error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -752,8 +751,46 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'tags' relationship."); + error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Cannot_add_with_object_data_in_ManyToMany_relationship() + { + // Arrange + WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.WorkItems.Add(existingWorkItem); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + } + }; + + string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; + + // 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: Expected an array in 'data' element, instead of an object."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index 52b814480c..813d13f1ec 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -806,7 +806,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_remove_with_null_data_in_OneToMany_relationship() + public async Task Cannot_remove_with_missing_data_in_OneToMany_relationship() { // Arrange WorkItem existingWorkItem = _fakers.WorkItem.Generate(); @@ -819,7 +819,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = (object)null }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; @@ -834,9 +833,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'subscribers' relationship."); - error.Source.Pointer.Should().Be("/data"); + error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -870,8 +869,46 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'tags' relationship."); + error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Cannot_remove_with_object_data_in_ManyToMany_relationship() + { + // Arrange + WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.WorkItems.Add(existingWorkItem); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + } + }; + + string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(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: Expected an array in 'data' element, instead of an object."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index d471beda96..1b912755e3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -683,7 +683,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_replace_with_null_data_in_OneToMany_relationship() + public async Task Cannot_replace_with_missing_data_in_OneToMany_relationship() { // Arrange WorkItem existingWorkItem = _fakers.WorkItem.Generate(); @@ -696,7 +696,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = (object)null }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; @@ -711,9 +710,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'subscribers' relationship."); - error.Source.Pointer.Should().Be("/data"); + error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -747,8 +746,46 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'tags' relationship."); + error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Cannot_replace_with_object_data_in_ManyToMany_relationship() + { + // Arrange + WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.WorkItems.Add(existingWorkItem); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + } + }; + + string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; + + // 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: Expected an array in 'data' element, instead of an object."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index 5bbf0e77f6..4bf00857dd 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -272,6 +272,86 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Source.Should().BeNull(); } + [Fact] + public async Task Cannot_create_for_missing_data() + { + // Arrange + WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + UserAccount existingUserAccount = _fakers.UserAccount.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.AddInRange(existingWorkItem, existingUserAccount); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + }; + + string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; + + // 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: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Cannot_create_for_array_data() + { + // Arrange + WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + UserAccount existingUserAccount = _fakers.UserAccount.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.AddInRange(existingWorkItem, existingUserAccount); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new[] + { + new + { + type = "userAccounts", + id = existingUserAccount.StringId + } + } + }; + + string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; + + // 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: Expected an object or 'null' in 'data' element, instead of an array."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + [Fact] public async Task Cannot_create_for_missing_type() { @@ -580,50 +660,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Source.Pointer.Should().Be("/data/type"); } - [Fact] - public async Task Cannot_create_with_data_array_in_relationship() - { - // Arrange - WorkItem existingWorkItem = _fakers.WorkItem.Generate(); - UserAccount existingUserAccount = _fakers.UserAccount.Generate(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.AddInRange(existingWorkItem, existingUserAccount); - await dbContext.SaveChangesAsync(); - }); - - var requestBody = new - { - data = new[] - { - new - { - type = "userAccounts", - id = existingUserAccount.StringId - } - } - }; - - string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; - - // 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: Expected single data element for to-one relationship."); - error.Detail.Should().Be("Expected single data element for 'assignee' relationship."); - error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); - } - [Fact] public async Task Can_clear_cyclic_relationship() { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index b95a150e44..a09e232ca7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -765,7 +765,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_replace_with_null_data_in_OneToMany_relationship() + public async Task Cannot_replace_with_missing_data_in_OneToMany_relationship() { // Arrange WorkItem existingWorkItem = _fakers.WorkItem.Generate(); @@ -786,7 +786,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { subscribers = new { - data = (object)null } } } @@ -804,9 +803,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'subscribers' relationship."); - error.Source.Pointer.Should().Be("/data/relationships/subscribers/data"); + error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data/relationships/subscribers"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } @@ -851,8 +850,57 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'tags' relationship."); + error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data/relationships/tags/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Cannot_replace_with_object_data_in_ManyToMany_relationship() + { + // 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.StringId, + relationships = new + { + tags = new + { + data = 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: Expected an array in 'data' element, instead of an object."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/tags/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 1b33f36a69..16b004ff9f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -703,7 +703,43 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_create_resource_for_data_array() + public async Task Cannot_update_resource_for_null_data() + { + // Arrange + WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.WorkItems.Add(existingWorkItem); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = (object)null + }; + + 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: Expected an object in 'data' element, instead of 'null'."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Cannot_update_resource_for_array_data() { // Arrange WorkItem existingWorkItem = _fakers.WorkItem.Generate(); @@ -738,7 +774,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Expected 'data' object instead of array."); + error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index a996632710..046b6b8e19 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -437,14 +437,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_create_for_missing_relationship_type() + public async Task Cannot_create_with_missing_data_in_relationship() { // Arrange WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + UserAccount existingUserAccount = _fakers.UserAccount.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.WorkItems.Add(existingWorkItem); + dbContext.AddInRange(existingWorkItem, existingUserAccount); await dbContext.SaveChangesAsync(); }); @@ -458,9 +459,60 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { assignee = new { - data = 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: The 'data' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data/relationships/assignee"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Cannot_create_with_array_data_in_relationship() + { + // Arrange + WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + UserAccount existingUserAccount = _fakers.UserAccount.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.AddInRange(existingWorkItem, existingUserAccount); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "workItems", + id = existingWorkItem.StringId, + relationships = new + { + assignee = new + { + data = new[] { - id = Unknown.StringId.For() + new + { + type = "userAccounts", + id = existingUserAccount.StringId + } } } } @@ -479,7 +531,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Title.Should().Be("Failed to deserialize request body: Expected an object or 'null' in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); @@ -487,7 +539,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_create_for_unknown_relationship_type() + public async Task Cannot_create_for_missing_relationship_type() { // Arrange WorkItem existingWorkItem = _fakers.WorkItem.Generate(); @@ -510,7 +562,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = Unknown.ResourceType, id = Unknown.StringId.For() } } @@ -530,15 +581,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); - error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); - error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); + error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] - public async Task Cannot_create_for_missing_relationship_ID() + public async Task Cannot_create_for_unknown_relationship_type() { // Arrange WorkItem existingWorkItem = _fakers.WorkItem.Generate(); @@ -561,7 +612,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "userAccounts" + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -580,15 +632,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); - error.Detail.Should().BeNull(); - error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); + error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] - public async Task Cannot_create_with_unknown_relationship_ID() + public async Task Cannot_create_for_missing_relationship_ID() { // Arrange WorkItem existingWorkItem = _fakers.WorkItem.Generate(); @@ -599,8 +651,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string userAccountId = Unknown.StringId.For(); - var requestBody = new { data = new @@ -613,8 +663,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "userAccounts", - id = userAccountId + type = "userAccounts" } } } @@ -627,19 +676,21 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); 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 '{userAccountId}' in relationship 'assignee' does not exist."); - error.Source.Should().BeNull(); + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); + error.Detail.Should().BeNull(); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] - public async Task Cannot_create_on_relationship_type_mismatch() + public async Task Cannot_create_with_unknown_relationship_ID() { // Arrange WorkItem existingWorkItem = _fakers.WorkItem.Generate(); @@ -650,6 +701,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId = Unknown.StringId.For(); + var requestBody = new { data = new @@ -662,8 +715,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "rgbColors", - id = "0A0B0C" + type = "userAccounts", + id = userAccountId } } } @@ -676,29 +729,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.Conflict); - error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); - error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'assignee'."); - error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + 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 '{userAccountId}' in relationship 'assignee' does not exist."); + error.Source.Should().BeNull(); } [Fact] - public async Task Cannot_create_with_data_array_in_relationship() + public async Task Cannot_create_on_relationship_type_mismatch() { // Arrange WorkItem existingWorkItem = _fakers.WorkItem.Generate(); - UserAccount existingUserAccount = _fakers.UserAccount.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { - dbContext.AddInRange(existingWorkItem, existingUserAccount); + dbContext.WorkItems.Add(existingWorkItem); await dbContext.SaveChangesAsync(); }); @@ -712,13 +762,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { assignee = new { - data = new[] + data = new { - new - { - type = "userAccounts", - id = existingUserAccount.StringId - } + type = "rgbColors", + id = "0A0B0C" } } } @@ -731,15 +778,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); 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: Expected single data element for to-one relationship."); - error.Detail.Should().Be("Expected single data element for 'assignee' relationship."); - error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); + error.StatusCode.Should().Be(HttpStatusCode.Conflict); + error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); + error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'assignee'."); + error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } From 2322ac8d3cb1f4e033fa282c2fb154ea2bffd6b9 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 30 Sep 2021 17:45:04 +0200 Subject: [PATCH 18/49] Unified remaining error messages --- ...annotClearRequiredRelationshipException.cs | 2 +- .../Serialization/DeserializationException.cs | 23 -------------- .../Serialization/JsonApiReader.cs | 30 +++++++++---------- .../RequestAdapters/ResourceObjectAdapter.cs | 25 ++++++++-------- .../Creating/AtomicCreateResourceTests.cs | 6 ++-- .../Resources/AtomicUpdateResourceTests.cs | 10 +++---- .../ReadWrite/Creating/CreateResourceTests.cs | 6 ++-- .../Updating/Resources/UpdateResourceTests.cs | 10 +++---- .../DefaultBehaviorTests.cs | 10 +++---- 9 files changed, 49 insertions(+), 73 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Serialization/DeserializationException.cs diff --git a/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs b/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs index 782cf1f2ea..4a7c6b5e66 100644 --- a/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs +++ b/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs @@ -14,7 +14,7 @@ public CannotClearRequiredRelationshipException(string relationshipName, string : base(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Failed to clear a required relationship.", - Detail = $"The relationship '{relationshipName}' of resource type '{resourceType}' " + + Detail = $"The relationship '{relationshipName}' on resource type '{resourceType}' " + $"with ID '{resourceId}' cannot be cleared because it is a required relationship." }) { diff --git a/src/JsonApiDotNetCore/Serialization/DeserializationException.cs b/src/JsonApiDotNetCore/Serialization/DeserializationException.cs deleted file mode 100644 index 49402c2f1b..0000000000 --- a/src/JsonApiDotNetCore/Serialization/DeserializationException.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using JsonApiDotNetCore.Serialization.RequestAdapters; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// The error that is thrown when deserialization of a JSON:API request body fails. - /// - internal sealed class DeserializationException : Exception - { - public string GenericMessage { get; } - public string SpecificMessage { get; } - public string SourcePointer { get; } - - public DeserializationException(RequestAdapterPosition position, string genericMessage, string specificMessage) - : base(genericMessage) - { - GenericMessage = genericMessage; - SpecificMessage = specificMessage; - SourcePointer = position?.ToSourcePointer(); - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index 6aff63ca5f..ac6ab8637c 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -64,20 +64,7 @@ private object GetModel(string requestBody) using IDisposable _ = CodeTimingSessionManager.Current.Measure("Read request body"); Document document = DeserializeDocument(requestBody, _options.SerializerReadOptions); - - try - { - return _documentAdapter.Convert(document); - } - catch (ModelConversionException exception) - { - throw new InvalidRequestBodyException(requestBody, exception.GenericMessage, exception.SpecificMessage, exception.SourcePointer, - exception.StatusCode, exception); - } - catch (DeserializationException exception) - { - throw new InvalidRequestBodyException(requestBody, exception.GenericMessage, exception.SpecificMessage, exception.SourcePointer); - } + return ConvertDocumentToModel(document, requestBody); } [AssertionMethod] @@ -94,8 +81,6 @@ private static void AssertHasRequestBody(string requestBody) private Document DeserializeDocument(string requestBody, JsonSerializerOptions serializerOptions) { - ArgumentGuard.NotNull(requestBody, nameof(requestBody)); - try { using IDisposable _ = @@ -111,5 +96,18 @@ private Document DeserializeDocument(string requestBody, JsonSerializerOptions s throw new InvalidRequestBodyException(requestBody, null, exception.Message, null, null, exception); } } + + private object ConvertDocumentToModel(Document document, string requestBody) + { + try + { + return _documentAdapter.Convert(document); + } + catch (ModelConversionException exception) + { + throw new InvalidRequestBodyException(requestBody, exception.GenericMessage, exception.SpecificMessage, exception.SourcePointer, + exception.StatusCode, exception); + } + } } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs index dbc54a0489..91dca72057 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs @@ -66,9 +66,9 @@ private void ConvertAttribute(IIdentifiable resource, string attributeName, obje AssertIsKnownAttribute(attr, attributeName, resourceContext, state); AssertNoInvalidAttribute(attributeValue, state); - AssertNoBlockedCreate(attr, state); - AssertNoBlockedChange(attr, state); - AssertNotReadOnly(attr, state); + AssertNoBlockedCreate(attr, resourceContext, state); + AssertNoBlockedChange(attr, resourceContext, state); + AssertNotReadOnly(attr, resourceContext, state); attr!.SetValue(resource, attributeValue); state.WritableTargetedFields.Attributes.Add(attr); @@ -90,7 +90,7 @@ private static void AssertNoInvalidAttribute(object attributeValue, RequestAdapt { if (info == JsonInvalidAttributeInfo.Id) { - throw new DeserializationException(state.Position, null, "Resource ID is read-only."); + throw new ModelConversionException(state.Position, "Resource ID is read-only.", null); } string typeName = info.AttributeType.GetFriendlyTypeName(); @@ -100,29 +100,30 @@ private static void AssertNoInvalidAttribute(object attributeValue, RequestAdapt } } - private static void AssertNoBlockedCreate(AttrAttribute attr, RequestAdapterState state) + private static void AssertNoBlockedCreate(AttrAttribute attr, ResourceContext resourceContext, RequestAdapterState state) { if (state.Request.WriteOperation == WriteOperationKind.CreateResource && !attr.Capabilities.HasFlag(AttrCapabilities.AllowCreate)) { - throw new DeserializationException(state.Position, "Setting the initial value of the requested attribute is not allowed.", - $"Setting the initial value of '{attr.PublicName}' is not allowed."); + throw new ModelConversionException(state.Position, "Attribute value cannot be assigned when creating resource.", + $"The attribute '{attr.PublicName}' on resource type '{resourceContext.PublicName}' cannot be assigned to."); } } - private static void AssertNoBlockedChange(AttrAttribute attr, RequestAdapterState state) + private static void AssertNoBlockedChange(AttrAttribute attr, ResourceContext resourceContext, RequestAdapterState state) { if (state.Request.WriteOperation == WriteOperationKind.UpdateResource && !attr.Capabilities.HasFlag(AttrCapabilities.AllowChange)) { - throw new DeserializationException(state.Position, "Changing the value of the requested attribute is not allowed.", - $"Changing the value of '{attr.PublicName}' is not allowed."); + throw new ModelConversionException(state.Position, "Attribute value cannot be assigned when updating resource.", + $"The attribute '{attr.PublicName}' on resource type '{resourceContext.PublicName}' cannot be assigned to."); } } - private static void AssertNotReadOnly(AttrAttribute attr, RequestAdapterState state) + private static void AssertNotReadOnly(AttrAttribute attr, ResourceContext resourceContext, RequestAdapterState state) { if (attr.Property.SetMethod == null) { - throw new DeserializationException(state.Position, "Attribute is read-only.", $"Attribute '{attr.PublicName}' is read-only."); + throw new ModelConversionException(state.Position, "Attribute is read-only.", + $"Attribute '{attr.PublicName}' on resource type '{resourceContext.PublicName}' is read-only."); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index dcd8ef7501..ab1fab1a44 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -754,8 +754,8 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() 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."); + error.Title.Should().Be("Failed to deserialize request body: Attribute value cannot be assigned when creating resource."); + error.Detail.Should().Be("The attribute 'createdAt' on resource type 'lyrics' cannot be assigned to."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/createdAt"); } @@ -798,7 +798,7 @@ public async Task Cannot_create_resource_with_readonly_attribute() 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."); + error.Detail.Should().Be("Attribute 'isArchived' on resource type 'playlists' is read-only."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/isArchived"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index e4a0fa1e96..aa978a0ffe 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -1507,8 +1507,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); + error.Title.Should().Be("Failed to deserialize request body: Attribute value cannot be assigned when updating resource."); + error.Detail.Should().Be("The attribute 'createdAt' on resource type 'lyrics' cannot be assigned to."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/createdAt"); } @@ -1557,7 +1557,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); + error.Detail.Should().Be("Attribute 'isArchived' on resource type 'playlists' is read-only."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/isArchived"); } @@ -1605,8 +1605,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().Be("Resource ID is read-only."); + error.Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/id"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 576788d8ab..2f500eef5a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -730,8 +730,8 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() 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 'isImportant' is not allowed."); + error.Title.Should().Be("Failed to deserialize request body: Attribute value cannot be assigned when creating resource."); + error.Detail.Should().Be("The attribute 'isImportant' on resource type 'workItems' cannot be assigned to."); error.Source.Pointer.Should().Be("/data/attributes/isImportant"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); @@ -766,7 +766,7 @@ public async Task Cannot_create_resource_with_readonly_attribute() 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 'isDeprecated' is read-only."); + error.Detail.Should().Be("Attribute 'isDeprecated' on resource type 'workItemGroups' is read-only."); error.Source.Pointer.Should().Be("/data/attributes/isDeprecated"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 16b004ff9f..3e5bd5277d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -1076,8 +1076,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'isImportant' is not allowed."); + error.Title.Should().Be("Failed to deserialize request body: Attribute value cannot be assigned when updating resource."); + error.Detail.Should().Be("The attribute 'isImportant' on resource type 'workItems' cannot be assigned to."); error.Source.Pointer.Should().Be("/data/attributes/isImportant"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); @@ -1121,7 +1121,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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 'isDeprecated' is read-only."); + error.Detail.Should().Be("Attribute 'isDeprecated' on resource type 'workItemGroups' is read-only."); error.Source.Pointer.Should().Be("/data/attributes/isDeprecated"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); @@ -1197,8 +1197,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().Be("Resource ID is read-only."); + error.Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); + error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/attributes/id"); responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs index b5c193d9bf..0fe047f5b5 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs @@ -212,7 +212,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); - error.Detail.Should().Be($"The relationship 'customer' of resource type 'orders' with ID '{existingOrder.StringId}' " + + error.Detail.Should().Be($"The relationship 'customer' on resource type 'orders' with ID '{existingOrder.StringId}' " + "cannot be cleared because it is a required relationship."); } @@ -249,7 +249,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); - error.Detail.Should().Be($"The relationship 'customer' of resource type 'orders' with ID '{existingOrder.StringId}' " + + error.Detail.Should().Be($"The relationship 'customer' on resource type 'orders' with ID '{existingOrder.StringId}' " + "cannot be cleared because it is a required relationship."); } @@ -297,7 +297,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); - error.Detail.Should().Be($"The relationship 'orders' of resource type 'customers' with ID '{existingOrder.StringId}' " + + error.Detail.Should().Be($"The relationship 'orders' on resource type 'customers' with ID '{existingOrder.StringId}' " + "cannot be cleared because it is a required relationship."); } @@ -334,7 +334,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); - error.Detail.Should().Be($"The relationship 'orders' of resource type 'customers' with ID '{existingOrder.StringId}' " + + error.Detail.Should().Be($"The relationship 'orders' on resource type 'customers' with ID '{existingOrder.StringId}' " + "cannot be cleared because it is a required relationship."); } @@ -378,7 +378,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); - error.Detail.Should().Be($"The relationship 'orders' of resource type 'customers' with ID '{existingOrder.StringId}' " + + error.Detail.Should().Be($"The relationship 'orders' on resource type 'customers' with ID '{existingOrder.StringId}' " + "cannot be cleared because it is a required relationship."); } From d254ac95fa5f5d9388404923fe0d17d7648ba66b Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 30 Sep 2021 18:01:33 +0200 Subject: [PATCH 19/49] Sealed types and reduced dependencies --- .../Middleware/JsonApiInputFormatter.cs | 5 +++- .../Middleware/JsonApiOutputFormatter.cs | 2 +- .../Serialization/IJsonApiReader.cs | 5 ++-- .../Serialization/IJsonApiWriter.cs | 9 ++++--- .../Serialization/JsonApiReader.cs | 10 +++----- .../Serialization/JsonApiWriter.cs | 24 ++++++++----------- test/TestBuildingBlocks/IntegrationTest.cs | 2 +- 7 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs index 9672be0dea..9f43df18fd 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs @@ -22,7 +22,10 @@ public async Task ReadAsync(InputFormatterContext context) ArgumentGuard.NotNull(context, nameof(context)); var reader = context.HttpContext.RequestServices.GetRequiredService(); - return await reader.ReadAsync(context.HttpContext.Request); + + object model = await reader.ReadAsync(context.HttpContext.Request); + + return model == null ? await InputFormatterResult.NoValueAsync() : await InputFormatterResult.SuccessAsync(model); } } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs index bd66f66067..07e7163b25 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs @@ -22,7 +22,7 @@ public async Task WriteAsync(OutputFormatterWriteContext context) ArgumentGuard.NotNull(context, nameof(context)); var writer = context.HttpContext.RequestServices.GetRequiredService(); - await writer.WriteAsync(context); + await writer.WriteAsync(context.Object, context.HttpContext); } } } diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs index 5ae602f6e0..9aecc8b0b2 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs @@ -1,12 +1,11 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Formatters; namespace JsonApiDotNetCore.Serialization { /// - /// Deserializes the incoming JSON request body and converts it to models, which are passed to controller actions by ASP.NET Core on `FromBody` + /// Deserializes the incoming JSON:API request body and converts it to models, which are passed to controller actions by ASP.NET Core on `FromBody` /// parameters. /// [PublicAPI] @@ -15,6 +14,6 @@ public interface IJsonApiReader /// /// Reads an object from the request body. /// - Task ReadAsync(HttpRequest httpRequest); + Task ReadAsync(HttpRequest httpRequest); } } diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs index 52cc6713e1..453be1bdf0 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs @@ -1,15 +1,18 @@ using System.Threading.Tasks; using JetBrains.Annotations; -using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Serialization { /// - /// Serializes models into the outgoing JSON response body. + /// Serializes ASP.NET models into the outgoing JSON:API response body. /// [PublicAPI] public interface IJsonApiWriter { - Task WriteAsync(OutputFormatterWriteContext context); + /// + /// Writes an object to the response body. + /// + Task WriteAsync(object model, HttpContext httpContext); } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index ac6ab8637c..97fa854c65 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -12,14 +12,12 @@ using JsonApiDotNetCore.Serialization.RequestAdapters; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Serialization { /// - [PublicAPI] - public class JsonApiReader : IJsonApiReader + public sealed class JsonApiReader : IJsonApiReader { private readonly IJsonApiOptions _options; private readonly IDocumentAdapter _documentAdapter; @@ -37,14 +35,12 @@ public JsonApiReader(IJsonApiOptions options, IDocumentAdapter documentAdapter, } /// - public async Task ReadAsync(HttpRequest httpRequest) + public async Task ReadAsync(HttpRequest httpRequest) { ArgumentGuard.NotNull(httpRequest, nameof(httpRequest)); string requestBody = await GetRequestBodyAsync(httpRequest); - object model = GetModel(requestBody); - - return model == null ? await InputFormatterResult.NoValueAsync() : await InputFormatterResult.SuccessAsync(model); + return GetModel(requestBody); } private async Task GetRequestBodyAsync(HttpRequest httpRequest) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs index 5ca93865c7..9c96ea935a 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs @@ -6,7 +6,6 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; -using JetBrains.Annotations; using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; @@ -14,18 +13,14 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; namespace JsonApiDotNetCore.Serialization { - /// - /// Formats the response data used (see https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-3.0). It was intended to - /// have as little dependencies as possible in formatting layer for greater extensibility. - /// - [PublicAPI] - public class JsonApiWriter : IJsonApiWriter + /// + public sealed class JsonApiWriter : IJsonApiWriter { private readonly IJsonApiSerializer _serializer; private readonly IExceptionHandler _exceptionHandler; @@ -45,21 +40,22 @@ public JsonApiWriter(IJsonApiSerializer serializer, IExceptionHandler exceptionH _traceWriter = new TraceLogWriter(loggerFactory); } - public async Task WriteAsync(OutputFormatterWriteContext context) + /// + public async Task WriteAsync(object model, HttpContext httpContext) { - ArgumentGuard.NotNull(context, nameof(context)); + ArgumentGuard.NotNull(httpContext, nameof(httpContext)); using IDisposable _ = CodeTimingSessionManager.Current.Measure("Write response body"); - HttpRequest request = context.HttpContext.Request; - HttpResponse response = context.HttpContext.Response; + HttpRequest request = httpContext.Request; + HttpResponse response = httpContext.Response; - await using TextWriter writer = context.WriterFactory(response.Body, Encoding.UTF8); + await using TextWriter writer = new HttpResponseStreamWriter(response.Body, Encoding.UTF8); string responseContent; try { - responseContent = SerializeResponse(context.Object, (HttpStatusCode)response.StatusCode); + responseContent = SerializeResponse(model, (HttpStatusCode)response.StatusCode); } #pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException catch (Exception exception) diff --git a/test/TestBuildingBlocks/IntegrationTest.cs b/test/TestBuildingBlocks/IntegrationTest.cs index 59e45b0b72..1d4eeb8923 100644 --- a/test/TestBuildingBlocks/IntegrationTest.cs +++ b/test/TestBuildingBlocks/IntegrationTest.cs @@ -9,7 +9,7 @@ namespace TestBuildingBlocks { /// - /// A base class for tests that conveniently enables to execute HTTP requests against json:api endpoints. + /// A base class for tests that conveniently enables to execute HTTP requests against JSON:API endpoints. /// public abstract class IntegrationTest { From 0923c8b03f2361902c27b6e73c12dc3dff78fe0b Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 30 Sep 2021 18:23:07 +0200 Subject: [PATCH 20/49] Fixed broken test on linux --- .../IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs index 8b3d6cc5d0..601b7d84a8 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs @@ -150,7 +150,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Detail.Should().Be("Exception has been thrown by the target of an invocation."); IEnumerable stackTraceLines = ((JsonElement)error.Meta["stackTrace"]).EnumerateArray().Select(token => token.GetString()); - stackTraceLines.Should().ContainMatch("*at object System.Reflection.*"); + stackTraceLines.Should().ContainMatch("*ThrowingArticle*"); responseDocument.Meta.Should().BeNull(); From 6e0d28726c7745b371001681b846b057c93878c6 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 1 Oct 2021 09:54:49 +0200 Subject: [PATCH 21/49] Adapter renames: - IOperationsDocumentAdapter -> IDocumentInOperationsRequestAdapter - IResourceDocumentAdapter -> IDocumentInResourceOrRelationshipRequestAdapter - IOperationResourceDataAdapter -> IResourceDataInOperationsRequestAdapter --- .../DeserializationBenchmarkBase.cs | 6 +++--- .../JsonApiApplicationBuilder.cs | 6 +++--- .../AtomicOperationObjectAdapter.cs | 10 ++++----- .../RequestAdapters/DocumentAdapter.cs | 21 ++++++++++--------- ... => DocumentInOperationsRequestAdapter.cs} | 4 ++-- ...InResourceOrRelationshipRequestAdapter.cs} | 5 +++-- ...=> IDocumentInOperationsRequestAdapter.cs} | 2 +- ...InResourceOrRelationshipRequestAdapter.cs} | 2 +- ...ResourceDataInOperationsRequestAdapter.cs} | 2 +- ...ResourceDataInOperationsRequestAdapter.cs} | 6 +++--- .../Serialization/InputConversionTests.cs | 6 +++--- 11 files changed, 36 insertions(+), 34 deletions(-) rename src/JsonApiDotNetCore/Serialization/RequestAdapters/{OperationsDocumentAdapter.cs => DocumentInOperationsRequestAdapter.cs} (92%) rename src/JsonApiDotNetCore/Serialization/RequestAdapters/{ResourceDocumentAdapter.cs => DocumentInResourceOrRelationshipRequestAdapter.cs} (90%) rename src/JsonApiDotNetCore/Serialization/RequestAdapters/{IOperationsDocumentAdapter.cs => IDocumentInOperationsRequestAdapter.cs} (90%) rename src/JsonApiDotNetCore/Serialization/RequestAdapters/{IResourceDocumentAdapter.cs => IDocumentInResourceOrRelationshipRequestAdapter.cs} (86%) rename src/JsonApiDotNetCore/Serialization/RequestAdapters/{IOperationResourceDataAdapter.cs => IResourceDataInOperationsRequestAdapter.cs} (90%) rename src/JsonApiDotNetCore/Serialization/RequestAdapters/{OperationResourceDataAdapter.cs => ResourceDataInOperationsRequestAdapter.cs} (73%) diff --git a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs index 191eb0a57a..59639512bb 100644 --- a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs +++ b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs @@ -42,13 +42,13 @@ protected DeserializationBenchmarkBase() var resourceDataAdapter = new ResourceDataAdapter(resourceDefinitionAccessor, resourceObjectAdapter); var atomicReferenceAdapter = new AtomicReferenceAdapter(resourceGraph, resourceFactory); - var atomicOperationResourceDataAdapter = new OperationResourceDataAdapter(resourceDefinitionAccessor, resourceObjectAdapter); + var atomicOperationResourceDataAdapter = new ResourceDataInOperationsRequestAdapter(resourceDefinitionAccessor, resourceObjectAdapter); var atomicOperationObjectAdapter = new AtomicOperationObjectAdapter(resourceGraph, options, atomicReferenceAdapter, atomicOperationResourceDataAdapter, relationshipDataAdapter); - var resourceDocumentAdapter = new ResourceDocumentAdapter(options, resourceDataAdapter, relationshipDataAdapter); - var operationsDocumentAdapter = new OperationsDocumentAdapter(options, atomicOperationObjectAdapter); + var resourceDocumentAdapter = new DocumentInResourceOrRelationshipRequestAdapter(options, resourceDataAdapter, relationshipDataAdapter); + var operationsDocumentAdapter = new DocumentInOperationsRequestAdapter(options, atomicOperationObjectAdapter); DocumentAdapter = new DocumentAdapter(request, targetedFields, resourceDocumentAdapter, operationsDocumentAdapter); } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 996030fe08..791b183912 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -268,10 +268,10 @@ private void AddSerializationLayer() _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(); + _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); _services.AddScoped(); } diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs index 92b20ae1ed..90d106f510 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs @@ -10,25 +10,25 @@ namespace JsonApiDotNetCore.Serialization.RequestAdapters /// public sealed class AtomicOperationObjectAdapter : IAtomicOperationObjectAdapter { - private readonly IOperationResourceDataAdapter _operationResourceDataAdapter; + private readonly IResourceDataInOperationsRequestAdapter _resourceDataInOperationsRequestAdapter; private readonly IAtomicReferenceAdapter _atomicReferenceAdapter; private readonly IRelationshipDataAdapter _relationshipDataAdapter; private readonly IResourceGraph _resourceGraph; private readonly IJsonApiOptions _options; public AtomicOperationObjectAdapter(IResourceGraph resourceGraph, IJsonApiOptions options, IAtomicReferenceAdapter atomicReferenceAdapter, - IOperationResourceDataAdapter operationResourceDataAdapter, IRelationshipDataAdapter relationshipDataAdapter) + IResourceDataInOperationsRequestAdapter resourceDataInOperationsRequestAdapter, IRelationshipDataAdapter relationshipDataAdapter) { ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(atomicReferenceAdapter, nameof(atomicReferenceAdapter)); - ArgumentGuard.NotNull(operationResourceDataAdapter, nameof(operationResourceDataAdapter)); + ArgumentGuard.NotNull(resourceDataInOperationsRequestAdapter, nameof(resourceDataInOperationsRequestAdapter)); ArgumentGuard.NotNull(relationshipDataAdapter, nameof(relationshipDataAdapter)); _resourceGraph = resourceGraph; _options = options; _atomicReferenceAdapter = atomicReferenceAdapter; - _operationResourceDataAdapter = operationResourceDataAdapter; + _resourceDataInOperationsRequestAdapter = resourceDataInOperationsRequestAdapter; _relationshipDataAdapter = relationshipDataAdapter; } @@ -51,7 +51,7 @@ public OperationContainer Convert(AtomicOperationObject atomicOperationObject, R if (writeOperation == WriteOperationKind.CreateResource || writeOperation == WriteOperationKind.UpdateResource) { - primaryResource = _operationResourceDataAdapter.Convert(atomicOperationObject.Data, requirements, state); + primaryResource = _resourceDataInOperationsRequestAdapter.Convert(atomicOperationObject.Data, requirements, state); } return new OperationContainer(writeOperation, primaryResource, state.WritableTargetedFields, state.Request); diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentAdapter.cs index 4f0848546b..ff8f99c7d3 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentAdapter.cs @@ -9,21 +9,22 @@ public sealed class DocumentAdapter : IDocumentAdapter { private readonly IJsonApiRequest _request; private readonly ITargetedFields _targetedFields; - private readonly IResourceDocumentAdapter _resourceDocumentAdapter; - private readonly IOperationsDocumentAdapter _operationsDocumentAdapter; + private readonly IDocumentInResourceOrRelationshipRequestAdapter _documentInResourceOrRelationshipRequestAdapter; + private readonly IDocumentInOperationsRequestAdapter _documentInOperationsRequestAdapter; - public DocumentAdapter(IJsonApiRequest request, ITargetedFields targetedFields, IResourceDocumentAdapter resourceDocumentAdapter, - IOperationsDocumentAdapter operationsDocumentAdapter) + public DocumentAdapter(IJsonApiRequest request, ITargetedFields targetedFields, + IDocumentInResourceOrRelationshipRequestAdapter documentInResourceOrRelationshipRequestAdapter, + IDocumentInOperationsRequestAdapter documentInOperationsRequestAdapter) { ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); - ArgumentGuard.NotNull(resourceDocumentAdapter, nameof(resourceDocumentAdapter)); - ArgumentGuard.NotNull(operationsDocumentAdapter, nameof(operationsDocumentAdapter)); + ArgumentGuard.NotNull(documentInResourceOrRelationshipRequestAdapter, nameof(documentInResourceOrRelationshipRequestAdapter)); + ArgumentGuard.NotNull(documentInOperationsRequestAdapter, nameof(documentInOperationsRequestAdapter)); _request = request; _targetedFields = targetedFields; - _resourceDocumentAdapter = resourceDocumentAdapter; - _operationsDocumentAdapter = operationsDocumentAdapter; + _documentInResourceOrRelationshipRequestAdapter = documentInResourceOrRelationshipRequestAdapter; + _documentInOperationsRequestAdapter = documentInOperationsRequestAdapter; } /// @@ -34,8 +35,8 @@ public object Convert(Document document) using var context = new RequestAdapterState(_request, _targetedFields); return context.Request.Kind == EndpointKind.AtomicOperations - ? _operationsDocumentAdapter.Convert(document, context) - : _resourceDocumentAdapter.Convert(document, context); + ? _documentInOperationsRequestAdapter.Convert(document, context) + : _documentInResourceOrRelationshipRequestAdapter.Convert(document, context); } } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentInOperationsRequestAdapter.cs similarity index 92% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs rename to src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentInOperationsRequestAdapter.cs index 51a7d3ba90..4633bd803a 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationsDocumentAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentInOperationsRequestAdapter.cs @@ -7,12 +7,12 @@ namespace JsonApiDotNetCore.Serialization.RequestAdapters { /// - public sealed class OperationsDocumentAdapter : IOperationsDocumentAdapter + public sealed class DocumentInOperationsRequestAdapter : IDocumentInOperationsRequestAdapter { private readonly IJsonApiOptions _options; private readonly IAtomicOperationObjectAdapter _atomicOperationObjectAdapter; - public OperationsDocumentAdapter(IJsonApiOptions options, IAtomicOperationObjectAdapter atomicOperationObjectAdapter) + public DocumentInOperationsRequestAdapter(IJsonApiOptions options, IAtomicOperationObjectAdapter atomicOperationObjectAdapter) { ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(atomicOperationObjectAdapter, nameof(atomicOperationObjectAdapter)); diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentInResourceOrRelationshipRequestAdapter.cs similarity index 90% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDocumentAdapter.cs rename to src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentInResourceOrRelationshipRequestAdapter.cs index 3b9043e176..abb0c91ed8 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDocumentAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentInResourceOrRelationshipRequestAdapter.cs @@ -7,13 +7,14 @@ namespace JsonApiDotNetCore.Serialization.RequestAdapters { /// - public sealed class ResourceDocumentAdapter : IResourceDocumentAdapter + public sealed class DocumentInResourceOrRelationshipRequestAdapter : IDocumentInResourceOrRelationshipRequestAdapter { private readonly IJsonApiOptions _options; private readonly IResourceDataAdapter _resourceDataAdapter; private readonly IRelationshipDataAdapter _relationshipDataAdapter; - public ResourceDocumentAdapter(IJsonApiOptions options, IResourceDataAdapter resourceDataAdapter, IRelationshipDataAdapter relationshipDataAdapter) + public DocumentInResourceOrRelationshipRequestAdapter(IJsonApiOptions options, IResourceDataAdapter resourceDataAdapter, + IRelationshipDataAdapter relationshipDataAdapter) { ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(resourceDataAdapter, nameof(resourceDataAdapter)); diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationsDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentInOperationsRequestAdapter.cs similarity index 90% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationsDocumentAdapter.cs rename to src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentInOperationsRequestAdapter.cs index 9e4f34c208..5d96e4d163 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationsDocumentAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentInOperationsRequestAdapter.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.Serialization.RequestAdapters /// /// Validates and converts a belonging to an atomic:operations request. /// - public interface IOperationsDocumentAdapter + public interface IDocumentInOperationsRequestAdapter { /// /// Validates and converts the specified . diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentInResourceOrRelationshipRequestAdapter.cs similarity index 86% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDocumentAdapter.cs rename to src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentInResourceOrRelationshipRequestAdapter.cs index 8ac5d119b6..4ea2dcdd28 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDocumentAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentInResourceOrRelationshipRequestAdapter.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Serialization.RequestAdapters /// /// Validates and converts a belonging to a resource or relationship request. /// - public interface IResourceDocumentAdapter + public interface IDocumentInResourceOrRelationshipRequestAdapter { /// /// Validates and converts the specified . diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationResourceDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataInOperationsRequestAdapter.cs similarity index 90% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationResourceDataAdapter.cs rename to src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataInOperationsRequestAdapter.cs index bc3615bad6..95c539ea1b 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IOperationResourceDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataInOperationsRequestAdapter.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.Serialization.RequestAdapters /// /// Validates and converts the data from an entry in an atomic:operations request that creates or updates a resource. /// - public interface IOperationResourceDataAdapter + public interface IResourceDataInOperationsRequestAdapter { /// /// Validates and converts the specified . diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationResourceDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataInOperationsRequestAdapter.cs similarity index 73% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationResourceDataAdapter.cs rename to src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataInOperationsRequestAdapter.cs index 02c641dbeb..401f780b58 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/OperationResourceDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataInOperationsRequestAdapter.cs @@ -4,10 +4,10 @@ namespace JsonApiDotNetCore.Serialization.RequestAdapters { - /// - public sealed class OperationResourceDataAdapter : ResourceDataAdapter, IOperationResourceDataAdapter + /// + public sealed class ResourceDataInOperationsRequestAdapter : ResourceDataAdapter, IResourceDataInOperationsRequestAdapter { - public OperationResourceDataAdapter(IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectAdapter resourceObjectAdapter) + public ResourceDataInOperationsRequestAdapter(IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectAdapter resourceObjectAdapter) : base(resourceDefinitionAccessor, resourceObjectAdapter) { } diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs index ef0c0af9d1..1b479e2684 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs @@ -259,13 +259,13 @@ private static DocumentAdapter CreateDocumentAdapter(Func Date: Fri, 1 Oct 2021 11:48:11 +0200 Subject: [PATCH 22/49] Added missing assertions on request body in error meta --- .../Creating/AtomicCreateResourceTests.cs | 20 +++++++++ ...reateResourceWithClientGeneratedIdTests.cs | 4 ++ ...eateResourceWithToManyRelationshipTests.cs | 14 +++++++ ...reateResourceWithToOneRelationshipTests.cs | 12 ++++++ .../Deleting/AtomicDeleteResourceTests.cs | 14 +++++++ .../Mixed/AtomicRequestBodyTests.cs | 4 ++ .../Mixed/MaximumOperationsPerRequestTests.cs | 2 + .../AtomicAddToToManyRelationshipTests.cs | 32 ++++++++++++++ ...AtomicRemoveFromToManyRelationshipTests.cs | 30 +++++++++++++ .../AtomicReplaceToManyRelationshipTests.cs | 32 ++++++++++++++ .../AtomicUpdateToOneRelationshipTests.cs | 30 +++++++++++++ .../AtomicReplaceToManyRelationshipTests.cs | 16 +++++++ .../Resources/AtomicUpdateResourceTests.cs | 42 +++++++++++++++++++ .../AtomicUpdateToOneRelationshipTests.cs | 14 +++++++ .../ReadWrite/Creating/CreateResourceTests.cs | 2 + ...reateResourceWithToOneRelationshipTests.cs | 2 + .../AddToToManyRelationshipTests.cs | 4 ++ .../RemoveFromToManyRelationshipTests.cs | 4 ++ .../ReplaceToManyRelationshipTests.cs | 2 + .../UpdateToOneRelationshipTests.cs | 2 + .../Updating/Resources/UpdateResourceTests.cs | 4 ++ 21 files changed, 286 insertions(+) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index ab1fab1a44..ed9f9efa99 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -497,6 +497,8 @@ public async Task Cannot_create_resource_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -533,6 +535,8 @@ public async Task Cannot_create_resource_for_ref_element() error.Title.Should().Be("Failed to deserialize request body: The 'relationship' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -565,6 +569,8 @@ public async Task Cannot_create_resource_for_missing_data() error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -598,6 +604,8 @@ public async Task Cannot_create_resource_for_null_data() error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -643,6 +651,8 @@ public async Task Cannot_create_resource_for_array_data() error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -681,6 +691,8 @@ public async Task Cannot_create_resource_for_missing_type() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -717,6 +729,8 @@ public async Task Cannot_create_resource_for_unknown_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -757,6 +771,8 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() error.Title.Should().Be("Failed to deserialize request body: Attribute value cannot be assigned when creating resource."); error.Detail.Should().Be("The attribute 'createdAt' on resource type 'lyrics' cannot be assigned to."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/createdAt"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -800,6 +816,8 @@ public async Task Cannot_create_resource_with_readonly_attribute() error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isArchived' on resource type 'playlists' is read-only."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/isArchived"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -840,6 +858,8 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() error.Title.Should().Be("Failed to deserialize request body: Incompatible attribute value found."); 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]/data/attributes/bornAt"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs index 910597d5e9..8f5b2d8081 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs @@ -228,6 +228,8 @@ public async Task Cannot_create_resource_for_incompatible_ID() error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -266,6 +268,8 @@ public async Task Cannot_create_resource_for_ID_and_local_ID() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index 55831bca96..ac9651faee 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -235,6 +235,8 @@ public async Task Cannot_create_for_missing_relationship_type() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -285,6 +287,8 @@ public async Task Cannot_create_for_unknown_relationship_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -334,6 +338,8 @@ public async Task Cannot_create_for_missing_relationship_ID() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -454,6 +460,8 @@ public async Task Cannot_create_on_relationship_type_mismatch() error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -572,6 +580,8 @@ public async Task Cannot_create_with_missing_data_in_OneToMany_relationship() error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -615,6 +625,8 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/tracks/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -660,6 +672,8 @@ public async Task Cannot_create_with_object_data_in_ManyToMany_relationship() error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/tracks/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index 083b516139..53f66aa50b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -296,6 +296,8 @@ public async Task Cannot_create_for_missing_data_in_relationship() error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -346,6 +348,8 @@ public async Task Cannot_create_for_array_data_in_relationship() error.Title.Should().Be("Failed to deserialize request body: Expected an object or 'null' in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -392,6 +396,8 @@ public async Task Cannot_create_for_missing_relationship_type() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -439,6 +445,8 @@ public async Task Cannot_create_for_unknown_relationship_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -485,6 +493,8 @@ public async Task Cannot_create_for_missing_relationship_ID() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -581,6 +591,8 @@ public async Task Cannot_create_on_relationship_type_mismatch() error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index d12f3a461f..44c5ba84dc 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -359,6 +359,8 @@ public async Task Cannot_delete_resource_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -391,6 +393,8 @@ public async Task Cannot_delete_resource_for_missing_ref_element() error.Title.Should().Be("Failed to deserialize request body: The 'ref' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -427,6 +431,8 @@ public async Task Cannot_delete_resource_for_missing_type() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -464,6 +470,8 @@ public async Task Cannot_delete_resource_for_unknown_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -500,6 +508,8 @@ public async Task Cannot_delete_resource_for_missing_ID() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -578,6 +588,8 @@ public async Task Cannot_delete_resource_for_incompatible_ID() error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int64'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -616,6 +628,8 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs index de2b72ba47..d1b1fd7a4a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs @@ -91,6 +91,8 @@ public async Task Cannot_process_for_missing_operations_array() error.Title.Should().Be("Failed to deserialize request body: No operations found."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -117,6 +119,8 @@ public async Task Cannot_process_empty_operations_array() error.Title.Should().Be("Failed to deserialize request body: No operations found."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs index 468d6dea73..8c95d0a15b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs @@ -75,6 +75,8 @@ public async Task Cannot_process_more_operations_than_maximum() error.Title.Should().Be("Failed to deserialize request body: Too many operations in request."); error.Detail.Should().Be("The number of operations in this request (3) is higher than the maximum of 2."); error.Source.Pointer.Should().Be("/atomic:operations"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index 8d63f24ee6..2355a326cb 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -73,6 +73,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this operation."); error.Detail.Should().Be("Relationship 'ownedBy' is not a to-many relationship."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -266,6 +268,8 @@ public async Task Cannot_add_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -303,6 +307,8 @@ public async Task Cannot_add_for_missing_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -341,6 +347,8 @@ public async Task Cannot_add_for_unknown_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -378,6 +386,8 @@ public async Task Cannot_add_for_missing_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -473,6 +483,8 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -510,6 +522,8 @@ public async Task Cannot_add_for_missing_relationship_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'relationship' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -548,6 +562,8 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -594,6 +610,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -641,6 +659,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -690,6 +710,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -735,6 +757,8 @@ public async Task Cannot_add_for_missing_type_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -781,6 +805,8 @@ public async Task Cannot_add_for_unknown_type_in_data() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -826,6 +852,8 @@ public async Task Cannot_add_for_missing_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -873,6 +901,8 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -998,6 +1028,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index b525a03785..80ba010c7b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -74,6 +74,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this operation."); error.Detail.Should().Be("Relationship 'ownedBy' is not a to-many relationship."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -266,6 +268,8 @@ public async Task Cannot_remove_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -303,6 +307,8 @@ public async Task Cannot_remove_for_missing_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -341,6 +347,8 @@ public async Task Cannot_remove_for_unknown_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -378,6 +386,8 @@ public async Task Cannot_remove_for_missing_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -473,6 +483,8 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -511,6 +523,8 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -557,6 +571,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -604,6 +620,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -653,6 +671,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -698,6 +718,8 @@ public async Task Cannot_remove_for_missing_type_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -744,6 +766,8 @@ public async Task Cannot_remove_for_unknown_type_in_data() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -789,6 +813,8 @@ public async Task Cannot_remove_for_missing_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -836,6 +862,8 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -961,6 +989,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index 92466e4b0f..8580e34e6f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -302,6 +302,8 @@ public async Task Cannot_replace_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -339,6 +341,8 @@ public async Task Cannot_replace_for_missing_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -377,6 +381,8 @@ public async Task Cannot_replace_for_unknown_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -414,6 +420,8 @@ public async Task Cannot_replace_for_missing_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -526,6 +534,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int16'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -565,6 +575,8 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -603,6 +615,8 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -649,6 +663,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -696,6 +712,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -745,6 +763,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -790,6 +810,8 @@ public async Task Cannot_replace_for_missing_type_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -836,6 +858,8 @@ public async Task Cannot_replace_for_unknown_type_in_data() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -881,6 +905,8 @@ public async Task Cannot_replace_for_missing_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -928,6 +954,8 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1053,6 +1081,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/id"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1107,6 +1137,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 6cd0db03ae..904e24e19c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -549,6 +549,8 @@ public async Task Cannot_create_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -586,6 +588,8 @@ public async Task Cannot_create_for_missing_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -624,6 +628,8 @@ public async Task Cannot_create_for_unknown_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -661,6 +667,8 @@ public async Task Cannot_create_for_missing_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -765,6 +773,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -804,6 +814,8 @@ public async Task Cannot_create_for_ID_and_local_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -842,6 +854,8 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -888,6 +902,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -942,6 +958,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an object or 'null' in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -984,6 +1002,8 @@ public async Task Cannot_create_for_missing_type_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1027,6 +1047,8 @@ public async Task Cannot_create_for_unknown_type_in_data() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1069,6 +1091,8 @@ public async Task Cannot_create_for_missing_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1113,6 +1137,8 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1217,6 +1243,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1268,6 +1296,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 1ef126694c..44c5c94a9c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -340,6 +340,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -392,6 +394,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -446,6 +450,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -496,6 +502,8 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/tracks/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -547,6 +555,8 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -597,6 +607,8 @@ public async Task Cannot_replace_for_missing_ID_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -649,6 +661,8 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -784,6 +798,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index aa978a0ffe..1100398738 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -634,6 +634,8 @@ public async Task Cannot_update_resource_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -738,6 +740,8 @@ public async Task Cannot_update_resource_for_missing_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -785,6 +789,8 @@ public async Task Cannot_update_resource_for_missing_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -834,6 +840,8 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -866,6 +874,8 @@ public async Task Cannot_update_resource_for_missing_data() error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -899,6 +909,8 @@ public async Task Cannot_update_resource_for_null_data() error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -951,6 +963,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -993,6 +1007,8 @@ public async Task Cannot_update_resource_for_missing_type_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1035,6 +1051,8 @@ public async Task Cannot_update_resource_for_missing_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1079,6 +1097,8 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1127,6 +1147,8 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1178,6 +1200,8 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() error.Title.Should().Be("Failed to deserialize request body: Conflicting 'id' values found."); error.Detail.Should().Be($"Expected '{performerId1}' instead of '{performerId2}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1226,6 +1250,8 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da error.Title.Should().Be("Failed to deserialize request body: Conflicting 'lid' values found."); error.Detail.Should().Be("Expected 'local-1' instead of 'local-2'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/lid"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1276,6 +1302,8 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1326,6 +1354,8 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da error.Title.Should().Be("Failed to deserialize request body: The 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1369,6 +1399,8 @@ public async Task Cannot_update_resource_for_unknown_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1461,6 +1493,8 @@ public async Task Cannot_update_resource_for_incompatible_ID() error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1510,6 +1544,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Attribute value cannot be assigned when updating resource."); error.Detail.Should().Be("The attribute 'createdAt' on resource type 'lyrics' cannot be assigned to."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/createdAt"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1559,6 +1595,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isArchived' on resource type 'playlists' is read-only."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/isArchived"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1608,6 +1646,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/id"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1657,6 +1697,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible attribute value found."); 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]/data/attributes/bornAt"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index ad7a3018f4..e9dd973b36 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -612,6 +612,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -671,6 +673,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an object or 'null' in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -718,6 +722,8 @@ public async Task Cannot_create_for_missing_type_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/track/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -766,6 +772,8 @@ public async Task Cannot_create_for_unknown_type_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -813,6 +821,8 @@ public async Task Cannot_create_for_missing_ID_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -862,6 +872,8 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -976,6 +988,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 2f500eef5a..c8111c110b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -700,6 +700,8 @@ public async Task Cannot_create_on_resource_type_mismatch_between_url_and_body() error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'workItems'."); error.Source.Pointer.Should().Be("/data/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index e75b3d2b0e..bb0bdf367b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -701,6 +701,8 @@ public async Task Cannot_create_resource_with_local_ID() error.Title.Should().Be("Failed to deserialize request body: The 'lid' element is not supported at this endpoint."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/lid"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index 625fd339bc..62254052ef 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -61,6 +61,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this endpoint."); error.Detail.Should().Be("Relationship 'assignee' is not a to-many relationship."); error.Source.Should().BeNull(); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -601,6 +603,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'userAccounts' is incompatible with type 'workTags' of relationship 'tags'."); error.Source.Pointer.Should().Be("/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index 813d13f1ec..b3db05142f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -76,6 +76,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this endpoint."); error.Detail.Should().Be("Relationship 'assignee' is not a to-many relationship."); error.Source.Should().BeNull(); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -717,6 +719,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'userAccounts' is incompatible with type 'workTags' of relationship 'tags'."); error.Source.Pointer.Should().Be("/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index 1b912755e3..78dd6ca290 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -629,6 +629,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'userAccounts' is incompatible with type 'workTags' of relationship 'tags'."); error.Source.Pointer.Should().Be("/data[0]/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index 4bf00857dd..002c9bb902 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -658,6 +658,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'assignee'."); error.Source.Pointer.Should().Be("/data/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 3e5bd5277d..f87523d4a1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -999,6 +999,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'workItems'."); error.Source.Pointer.Should().Be("/data/type"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1037,6 +1039,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Conflicting 'id' values found."); error.Detail.Should().Be($"Expected '{existingWorkItems[1].StringId}' instead of '{existingWorkItems[0].StringId}'."); error.Source.Pointer.Should().Be("/data/id"); + + responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] From 317f2938649918a296d4b4764a085c5d38ebfbc1 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 1 Oct 2021 15:25:50 +0200 Subject: [PATCH 23/49] Refactorings: - Changed deserializer to accept Error(s) instead of Document (used to be ErrorDocument, but they were merged) - Write request body in error meta instead of top-level meta - Fixed: Log at Error instead of Info when processing an operation throws unknown error --- .../AtomicOperations/OperationsProcessor.cs | 10 +- .../Controllers/CoreJsonApiController.cs | 14 +- .../Errors/FailedOperationException.cs | 27 +++ .../Errors/InvalidRequestBodyException.cs | 10 +- .../Middleware/AsyncJsonApiExceptionFilter.cs | 7 +- .../Middleware/ExceptionHandler.cs | 30 +-- .../Middleware/IExceptionHandler.cs | 3 +- .../AtomicOperationsResponseSerializer.cs | 19 +- .../Serialization/JsonApiReader.cs | 11 +- .../Serialization/JsonApiWriter.cs | 33 +--- .../Serialization/ModelConversionException.cs | 4 +- .../Serialization/Objects/Document.cs | 21 -- .../Serialization/Objects/ErrorObject.cs | 19 ++ .../Serialization/ResponseSerializer.cs | 19 +- .../Creating/AtomicCreateResourceTests.cs | 39 ++-- ...reateResourceWithClientGeneratedIdTests.cs | 7 +- ...eateResourceWithToManyRelationshipTests.cs | 21 +- ...reateResourceWithToOneRelationshipTests.cs | 19 +- .../Deleting/AtomicDeleteResourceTests.cs | 22 +-- .../Mixed/AtomicLoggingTests.cs | 186 ++++++++++++++++++ .../Mixed/AtomicRequestBodyTests.cs | 9 +- .../Mixed/MaximumOperationsPerRequestTests.cs | 3 +- .../AtomicAddToToManyRelationshipTests.cs | 49 ++--- ...AtomicRemoveFromToManyRelationshipTests.cs | 46 ++--- .../AtomicReplaceToManyRelationshipTests.cs | 49 ++--- .../AtomicUpdateToOneRelationshipTests.cs | 47 ++--- .../AtomicReplaceToManyRelationshipTests.cs | 24 +-- .../Resources/AtomicUpdateResourceTests.cs | 69 +++---- .../AtomicUpdateToOneRelationshipTests.cs | 22 +-- .../ActionResultTests.cs | 2 +- .../AlternateExceptionHandler.cs | 4 +- .../ExceptionHandlerTests.cs | 3 +- .../ReadWrite/Creating/CreateResourceTests.cs | 42 ++-- ...reateResourceWithClientGeneratedIdTests.cs | 1 + ...eateResourceWithToManyRelationshipTests.cs | 24 +-- ...reateResourceWithToOneRelationshipTests.cs | 22 +-- .../ReadWrite/Deleting/DeleteResourceTests.cs | 1 + .../AddToToManyRelationshipTests.cs | 29 ++- .../RemoveFromToManyRelationshipTests.cs | 30 ++- .../ReplaceToManyRelationshipTests.cs | 26 ++- .../UpdateToOneRelationshipTests.cs | 24 ++- .../ReplaceToManyRelationshipTests.cs | 21 +- .../Updating/Resources/UpdateResourceTests.cs | 52 ++--- .../Resources/UpdateToOneRelationshipTests.cs | 19 +- ...orDocumentTests.cs => ErrorObjectTests.cs} | 9 +- .../Server/ResponseSerializerTests.cs | 7 +- 46 files changed, 575 insertions(+), 580 deletions(-) create mode 100644 src/JsonApiDotNetCore/Errors/FailedOperationException.cs create mode 100644 test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs rename test/UnitTests/Internal/{ErrorDocumentTests.cs => ErrorObjectTests.cs} (79%) diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs index 9a142c0a4f..284abe7995 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs @@ -89,15 +89,7 @@ public virtual async Task> ProcessAsync(IList errors) { ArgumentGuard.NotNull(errors, nameof(errors)); - var document = new Document - { - Errors = errors.ToList() - }; + ErrorObject[] errorArray = errors.ToArray(); - return new ObjectResult(document) + return new ObjectResult(errorArray) { - StatusCode = (int)document.GetErrorStatusCode() + StatusCode = (int)ErrorObject.GetResponseStatusCode(errorArray) }; } } diff --git a/src/JsonApiDotNetCore/Errors/FailedOperationException.cs b/src/JsonApiDotNetCore/Errors/FailedOperationException.cs new file mode 100644 index 0000000000..4ae21ef469 --- /dev/null +++ b/src/JsonApiDotNetCore/Errors/FailedOperationException.cs @@ -0,0 +1,27 @@ +using System; +using System.Net; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Errors +{ + /// + /// The error that is thrown when an operation in an atomic:operations request failed to be processed for unknown reasons. + /// + [PublicAPI] + public sealed class FailedOperationException : JsonApiException + { + public FailedOperationException(int operationIndex, Exception innerException) + : base(new ErrorObject(HttpStatusCode.InternalServerError) + { + Title = "An unhandled error occurred while processing an operation in this request.", + Detail = innerException.Message, + Source = new ErrorSource + { + Pointer = $"/atomic:operations[{operationIndex}]" + } + }, innerException) + { + } + } +} diff --git a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs index 9051aa46d6..a508a82ad2 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net; using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; @@ -11,8 +12,6 @@ namespace JsonApiDotNetCore.Errors [PublicAPI] public sealed class InvalidRequestBodyException : JsonApiException { - public string RequestBody { get; } - public InvalidRequestBodyException(string requestBody, string genericMessage, string specificMessage, string sourcePointer, HttpStatusCode? alternativeStatusCode = null, Exception innerException = null) : base(new ErrorObject(alternativeStatusCode ?? HttpStatusCode.UnprocessableEntity) @@ -24,10 +23,15 @@ public InvalidRequestBodyException(string requestBody, string genericMessage, st : new ErrorSource { Pointer = sourcePointer + }, + Meta = string.IsNullOrEmpty(requestBody) + ? null + : new Dictionary + { + ["RequestBody"] = requestBody } }, innerException) { - RequestBody = requestBody; } } } diff --git a/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs index d7d6349b68..d82cbddeed 100644 --- a/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; @@ -26,11 +27,11 @@ public Task OnExceptionAsync(ExceptionContext context) if (context.HttpContext.IsJsonApiRequest()) { - Document document = _exceptionHandler.HandleException(context.Exception); + IReadOnlyList errors = _exceptionHandler.HandleException(context.Exception); - context.Result = new ObjectResult(document) + context.Result = new ObjectResult(errors) { - StatusCode = (int)document.GetErrorStatusCode() + StatusCode = (int)ErrorObject.GetResponseStatusCode(errors) }; } diff --git a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs index 205ca6a208..3d7630b9bf 100644 --- a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs +++ b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs @@ -27,7 +27,7 @@ public ExceptionHandler(ILoggerFactory loggerFactory, IJsonApiOptions options) _logger = loggerFactory.CreateLogger(); } - public Document HandleException(Exception exception) + public IReadOnlyList HandleException(Exception exception) { ArgumentGuard.NotNull(exception, nameof(exception)); @@ -35,7 +35,7 @@ public Document HandleException(Exception exception) LogException(demystified); - return CreateErrorDocument(demystified); + return CreateErrorResponse(demystified); } private void LogException(Exception exception) @@ -55,7 +55,7 @@ protected virtual LogLevel GetLogLevel(Exception exception) return LogLevel.None; } - if (exception is JsonApiException) + if (exception is JsonApiException and not FailedOperationException) { return LogLevel.Information; } @@ -70,7 +70,7 @@ protected virtual string GetLogMessage(Exception exception) return exception is JsonApiException jsonApiException ? jsonApiException.GetSummary() : exception.Message; } - protected virtual Document CreateErrorDocument(Exception exception) + protected virtual IReadOnlyList CreateErrorResponse(Exception exception) { ArgumentGuard.NotNull(exception, nameof(exception)); @@ -84,25 +84,15 @@ protected virtual Document CreateErrorDocument(Exception exception) Detail = exception.Message }.AsArray(); - var document = new Document - { - Errors = errors.ToList() - }; - if (_options.IncludeExceptionStackTraceInErrors && exception is not InvalidModelStateException) { - IncludeStackTraces(exception, document.Errors); - } - - if (_options.IncludeRequestBodyInErrors && exception is InvalidRequestBodyException { RequestBody: { } } invalidRequestBodyException) - { - IncludeRequestBody(invalidRequestBodyException, document); + IncludeStackTraces(exception, errors); } - return document; + return errors; } - private void IncludeStackTraces(Exception exception, IList errors) + private void IncludeStackTraces(Exception exception, IReadOnlyList errors) { string[] stackTraceLines = exception.ToString().Split(Environment.NewLine); @@ -115,11 +105,5 @@ private void IncludeStackTraces(Exception exception, IList errors) } } } - - private static void IncludeRequestBody(InvalidRequestBodyException exception, Document document) - { - document.Meta ??= new Dictionary(); - document.Meta["RequestBody"] = exception.RequestBody; - } } } diff --git a/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs index 9f44e33a96..a962d8cfdd 100644 --- a/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs +++ b/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Middleware @@ -8,6 +9,6 @@ namespace JsonApiDotNetCore.Middleware /// public interface IExceptionHandler { - Document HandleException(Exception exception); + IReadOnlyList HandleException(Exception exception); } } diff --git a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs index a7892755c5..651ce87033 100644 --- a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs @@ -54,13 +54,28 @@ public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectB /// public string Serialize(object content) { - if (content is IList operations) + if (content is IEnumerable operations) { return SerializeOperationsDocument(operations); } - if (content is Document errorDocument) + if (content is IEnumerable errors) { + var errorDocument = new Document + { + Errors = errors.ToArray() + }; + + return SerializeErrorDocument(errorDocument); + } + + if (content is ErrorObject errorObject) + { + var errorDocument = new Document + { + Errors = errorObject.AsArray() + }; + return SerializeErrorDocument(errorDocument); } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index 97fa854c65..7fb8ddd7fd 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -68,10 +68,7 @@ private static void AssertHasRequestBody(string requestBody) { if (string.IsNullOrEmpty(requestBody)) { - throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) - { - Title = "Missing request body." - }); + throw new InvalidRequestBodyException(null, "Missing request body.", null, null, HttpStatusCode.BadRequest); } } @@ -89,7 +86,7 @@ private Document DeserializeDocument(string requestBody, JsonSerializerOptions s // 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 InvalidRequestBodyException(requestBody, null, exception.Message, null, null, exception); + throw new InvalidRequestBodyException(_options.IncludeRequestBodyInErrors ? requestBody : null, null, exception.Message, null, null, exception); } } @@ -101,8 +98,8 @@ private object ConvertDocumentToModel(Document document, string requestBody) } catch (ModelConversionException exception) { - throw new InvalidRequestBodyException(requestBody, exception.GenericMessage, exception.SpecificMessage, exception.SourcePointer, - exception.StatusCode, exception); + throw new InvalidRequestBodyException(_options.IncludeRequestBodyInErrors ? requestBody : null, exception.GenericMessage, + exception.SpecificMessage, exception.SourcePointer, exception.StatusCode, exception); } } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs index 9c96ea935a..59c6fa4cec 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; using System.Net.Http; using System.Text; @@ -61,10 +60,9 @@ public async Task WriteAsync(object model, HttpContext httpContext) catch (Exception exception) #pragma warning restore AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException { - Document document = _exceptionHandler.HandleException(exception); - responseContent = _serializer.Serialize(document); - - response.StatusCode = (int)document.GetErrorStatusCode(); + IReadOnlyList errors = _exceptionHandler.HandleException(exception); + responseContent = _serializer.Serialize(errors); + response.StatusCode = (int)ErrorObject.GetResponseStatusCode(errors); } bool hasMatchingETag = SetETagResponseHeader(request, response, responseContent); @@ -114,9 +112,7 @@ private string SerializeResponse(object contextObject, HttpStatusCode statusCode } } - object contextObjectWrapped = WrapErrors(contextObject); - - return _serializer.Serialize(contextObjectWrapped); + return _serializer.Serialize(contextObject); } private bool IsSuccessStatusCode(HttpStatusCode statusCode) @@ -124,27 +120,6 @@ private bool IsSuccessStatusCode(HttpStatusCode statusCode) return new HttpResponseMessage(statusCode).IsSuccessStatusCode; } - private static object WrapErrors(object contextObject) - { - if (contextObject is IEnumerable errors) - { - return new Document - { - Errors = errors.ToList() - }; - } - - if (contextObject is ErrorObject error) - { - return new Document - { - Errors = error.AsList() - }; - } - - return contextObject; - } - private bool SetETagResponseHeader(HttpRequest request, HttpResponse response, string responseContent) { bool isReadOnly = request.Method == HttpMethod.Get.Method || request.Method == HttpMethod.Head.Method; diff --git a/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs b/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs index 967597b0aa..ed0315a918 100644 --- a/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs +++ b/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.RequestAdapters; namespace JsonApiDotNetCore.Serialization @@ -7,7 +8,8 @@ namespace JsonApiDotNetCore.Serialization /// /// The error that is thrown when unable to convert a deserialized request body to an ASP.NET model. /// - internal sealed class ModelConversionException : Exception + [PublicAPI] + public sealed class ModelConversionException : Exception { public string GenericMessage { get; } public string SpecificMessage { get; } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs index ae3a09b9b1..995b2070e8 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs @@ -1,7 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Net; using System.Text.Json.Serialization; namespace JsonApiDotNetCore.Serialization.Objects @@ -42,23 +39,5 @@ public sealed class Document [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/ErrorObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorObject.cs index a5ac6be1a8..38326d2ae5 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorObject.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Text.Json.Serialization; using JetBrains.Annotations; @@ -55,5 +56,23 @@ public ErrorObject(HttpStatusCode statusCode) { StatusCode = statusCode; } + + public static HttpStatusCode GetResponseStatusCode(IReadOnlyList errorObjects) + { + if (errorObjects.IsNullOrEmpty()) + { + return HttpStatusCode.InternalServerError; + } + + int[] statusCodes = errorObjects.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/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs index 348e1d7a2d..567d14f5e6 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs @@ -71,12 +71,27 @@ public string Serialize(object content) return SerializeMany(collectionOfIdentifiable.ToArray()); } - if (content is Document errorDocument) + if (content is IEnumerable errorObjects) { + var errorDocument = new Document + { + Errors = errorObjects.ToArray() + }; + + return SerializeErrorDocument(errorDocument); + } + + if (content is ErrorObject errorObject) + { + var errorDocument = new Document + { + Errors = errorObject.AsArray() + }; + return SerializeErrorDocument(errorDocument); } - throw new InvalidOperationException("Data being returned must be errors or resources."); + throw new InvalidOperationException("Data being returned must be null, errors or resources."); } private string SerializeErrorDocument(Document document) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index ed9f9efa99..ad9ab8197d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -257,8 +257,7 @@ public async Task Cannot_create_resource_with_unknown_attribute() error.Title.Should().Be("Failed to deserialize request body: Unknown attribute found."); error.Detail.Should().Be("Attribute 'doesNotExist' does not exist on resource type 'playlists'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/doesNotExist"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -359,8 +358,7 @@ public async Task Cannot_create_resource_with_unknown_relationship() error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource type 'lyrics'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/doesNotExist"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -462,8 +460,7 @@ public async Task Cannot_create_resource_with_client_generated_ID() error.Title.Should().Be("Failed to deserialize request body: The use of client-generated IDs is disabled."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -497,8 +494,7 @@ public async Task Cannot_create_resource_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -535,8 +531,7 @@ public async Task Cannot_create_resource_for_ref_element() error.Title.Should().Be("Failed to deserialize request body: The 'relationship' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -569,8 +564,7 @@ public async Task Cannot_create_resource_for_missing_data() error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -604,8 +598,7 @@ public async Task Cannot_create_resource_for_null_data() error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -651,8 +644,7 @@ public async Task Cannot_create_resource_for_array_data() error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -691,8 +683,7 @@ public async Task Cannot_create_resource_for_missing_type() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -729,8 +720,7 @@ public async Task Cannot_create_resource_for_unknown_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -771,8 +761,7 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() error.Title.Should().Be("Failed to deserialize request body: Attribute value cannot be assigned when creating resource."); error.Detail.Should().Be("The attribute 'createdAt' on resource type 'lyrics' cannot be assigned to."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/createdAt"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -816,8 +805,7 @@ public async Task Cannot_create_resource_with_readonly_attribute() error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isArchived' on resource type 'playlists' is read-only."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/isArchived"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -858,8 +846,7 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() error.Title.Should().Be("Failed to deserialize request body: Incompatible attribute value found."); 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]/data/attributes/bornAt"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs index 8f5b2d8081..1574e20095 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs @@ -186,6 +186,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -228,8 +229,7 @@ public async Task Cannot_create_resource_for_incompatible_ID() error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -268,8 +268,7 @@ public async Task Cannot_create_resource_for_ID_and_local_ID() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index ac9651faee..c3952cf1c6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -235,8 +235,7 @@ public async Task Cannot_create_for_missing_relationship_type() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -287,8 +286,7 @@ public async Task Cannot_create_for_unknown_relationship_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -338,8 +336,7 @@ public async Task Cannot_create_for_missing_relationship_ID() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -460,8 +457,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -580,8 +576,7 @@ public async Task Cannot_create_with_missing_data_in_OneToMany_relationship() error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -625,8 +620,7 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/tracks/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -672,8 +666,7 @@ public async Task Cannot_create_with_object_data_in_ManyToMany_relationship() error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/tracks/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index 53f66aa50b..51c9dc27c9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -296,8 +296,7 @@ public async Task Cannot_create_for_missing_data_in_relationship() error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -348,8 +347,7 @@ public async Task Cannot_create_for_array_data_in_relationship() error.Title.Should().Be("Failed to deserialize request body: Expected an object or 'null' in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -396,8 +394,7 @@ public async Task Cannot_create_for_missing_relationship_type() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -445,8 +442,7 @@ public async Task Cannot_create_for_unknown_relationship_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -493,8 +489,7 @@ public async Task Cannot_create_for_missing_relationship_ID() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -544,6 +539,7 @@ public async Task Cannot_create_with_unknown_relationship_ID() error.Title.Should().Be("A related resource 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]"); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -591,8 +587,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index 44c5ba84dc..5eef754388 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -359,8 +359,7 @@ public async Task Cannot_delete_resource_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -393,8 +392,7 @@ public async Task Cannot_delete_resource_for_missing_ref_element() error.Title.Should().Be("Failed to deserialize request body: The 'ref' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -431,8 +429,7 @@ public async Task Cannot_delete_resource_for_missing_type() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -470,8 +467,7 @@ public async Task Cannot_delete_resource_for_unknown_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -508,8 +504,7 @@ public async Task Cannot_delete_resource_for_missing_ID() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -549,6 +544,7 @@ public async Task Cannot_delete_resource_for_unknown_ID() error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'performers' with ID '{performerId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -588,8 +584,7 @@ public async Task Cannot_delete_resource_for_incompatible_ID() error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int64'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -628,8 +623,7 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs new file mode 100644 index 0000000000..eb3bae022b --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs @@ -0,0 +1,186 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.AtomicOperations; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TestBuildingBlocks; +using Xunit; + +namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations.Mixed +{ + public sealed class AtomicLoggingTests : IClassFixture, OperationsDbContext>> + { + private readonly IntegrationTestContext, OperationsDbContext> _testContext; + + public AtomicLoggingTests(IntegrationTestContext, OperationsDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + + var loggerFactory = new FakeLoggerFactory(LogLevel.Information); + + testContext.ConfigureLogging(options => + { + options.ClearProviders(); + options.AddProvider(loggerFactory); + options.SetMinimumLevel(LogLevel.Information); + }); + + testContext.ConfigureServicesBeforeStartup(services => + { + services.AddSingleton(loggerFactory); + }); + + testContext.ConfigureServicesAfterStartup(services => + { + services.AddSingleton(); + }); + } + + [Fact] + public async Task Logs_at_error_level_on_unhandled_exception() + { + // Arrange + var loggerFactory = _testContext.Factory.Services.GetRequiredService(); + loggerFactory.Logger.Clear(); + + var transactionFactory = (ThrowingOperationsTransactionFactory)_testContext.Factory.Services.GetRequiredService(); + transactionFactory.ThrowOnOperationStart = true; + + var requestBody = new + { + atomic__operations = new object[] + { + new + { + op = "add", + data = new + { + type = "performers", + attributes = new + { + } + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); + + responseDocument.Errors.Should().HaveCount(1); + + ErrorObject error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + error.Title.Should().Be("An unhandled error occurred while processing an operation in this request."); + error.Detail.Should().Be("Simulated failure."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); + + loggerFactory.Logger.Messages.Should().NotBeEmpty(); + + loggerFactory.Logger.Messages.Should().ContainSingle(message => message.LogLevel == LogLevel.Error && + message.Text.Contains("Simulated failure.", StringComparison.Ordinal)); + } + + [Fact] + public async Task Logs_at_info_level_on_invalid_request_body() + { + // Arrange + var loggerFactory = _testContext.Factory.Services.GetRequiredService(); + loggerFactory.Logger.Clear(); + + var transactionFactory = (ThrowingOperationsTransactionFactory)_testContext.Factory.Services.GetRequiredService(); + transactionFactory.ThrowOnOperationStart = false; + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "update" + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + + loggerFactory.Logger.Messages.Should().NotBeEmpty(); + + loggerFactory.Logger.Messages.Should().ContainSingle(message => message.LogLevel == LogLevel.Information && + message.Text.Contains("Failed to deserialize request body", StringComparison.Ordinal)); + } + + private sealed class ThrowingOperationsTransactionFactory : IOperationsTransactionFactory + { + public bool ThrowOnOperationStart { get; set; } + + public Task BeginTransactionAsync(CancellationToken cancellationToken) + { + IOperationsTransaction transaction = new ThrowingOperationsTransaction(this); + return Task.FromResult(transaction); + } + + private sealed class ThrowingOperationsTransaction : IOperationsTransaction + { + private readonly ThrowingOperationsTransactionFactory _owner; + + public string TransactionId => "some"; + + public ThrowingOperationsTransaction(ThrowingOperationsTransactionFactory owner) + { + _owner = owner; + } + + public ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } + + public Task BeforeProcessOperationAsync(CancellationToken cancellationToken) + { + return ThrowIfEnabled(); + } + + public Task AfterProcessOperationAsync(CancellationToken cancellationToken) + { + return ThrowIfEnabled(); + } + + public Task CommitAsync(CancellationToken cancellationToken) + { + return ThrowIfEnabled(); + } + + private Task ThrowIfEnabled() + { + if (_owner.ThrowOnOperationStart) + { + throw new Exception("Simulated failure."); + } + + return Task.CompletedTask; + } + } + } + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs index d1b1fd7a4a..0b61701084 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs @@ -36,9 +36,10 @@ public async Task Cannot_process_for_missing_request_body() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Missing request body."); + error.Title.Should().Be("Failed to deserialize request body: Missing request body."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -91,8 +92,7 @@ public async Task Cannot_process_for_missing_operations_array() error.Title.Should().Be("Failed to deserialize request body: No operations found."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -119,8 +119,7 @@ public async Task Cannot_process_empty_operations_array() error.Title.Should().Be("Failed to deserialize request body: No operations found."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs index 8c95d0a15b..199f91f6ae 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs @@ -75,8 +75,7 @@ public async Task Cannot_process_more_operations_than_maximum() error.Title.Should().Be("Failed to deserialize request body: Too many operations in request."); error.Detail.Should().Be("The number of operations in this request (3) is higher than the maximum of 2."); error.Source.Pointer.Should().Be("/atomic:operations"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index 2355a326cb..c234c09182 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -73,8 +73,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this operation."); error.Detail.Should().Be("Relationship 'ownedBy' is not a to-many relationship."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -268,8 +267,7 @@ public async Task Cannot_add_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -307,8 +305,7 @@ public async Task Cannot_add_for_missing_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -347,8 +344,7 @@ public async Task Cannot_add_for_unknown_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -386,8 +382,7 @@ public async Task Cannot_add_for_missing_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -444,6 +439,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("The requested resource 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]"); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -483,8 +479,7 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -522,8 +517,7 @@ public async Task Cannot_add_for_missing_relationship_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'relationship' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -562,8 +556,7 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -610,8 +603,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -659,8 +651,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -710,8 +701,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -757,8 +747,7 @@ public async Task Cannot_add_for_missing_type_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -805,8 +794,7 @@ public async Task Cannot_add_for_unknown_type_in_data() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -852,8 +840,7 @@ public async Task Cannot_add_for_missing_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -901,8 +888,7 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1028,8 +1014,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index 80ba010c7b..95840ff95d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -74,8 +74,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this operation."); error.Detail.Should().Be("Relationship 'ownedBy' is not a to-many relationship."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -268,8 +267,7 @@ public async Task Cannot_remove_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -307,8 +305,7 @@ public async Task Cannot_remove_for_missing_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -347,8 +344,7 @@ public async Task Cannot_remove_for_unknown_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -386,8 +382,7 @@ public async Task Cannot_remove_for_missing_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -444,6 +439,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("The requested resource 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]"); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -483,8 +479,7 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -523,8 +518,7 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -571,8 +565,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -620,8 +613,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -671,8 +663,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -718,8 +709,7 @@ public async Task Cannot_remove_for_missing_type_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -766,8 +756,7 @@ public async Task Cannot_remove_for_unknown_type_in_data() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -813,8 +802,7 @@ public async Task Cannot_remove_for_missing_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -862,8 +850,7 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -989,8 +976,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index 8580e34e6f..c7b6972438 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -302,8 +302,7 @@ public async Task Cannot_replace_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -341,8 +340,7 @@ public async Task Cannot_replace_for_missing_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -381,8 +379,7 @@ public async Task Cannot_replace_for_unknown_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -420,8 +417,7 @@ public async Task Cannot_replace_for_missing_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -478,6 +474,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("The requested resource 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]"); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -534,8 +531,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int16'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -575,8 +571,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -615,8 +610,7 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -663,8 +657,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -712,8 +705,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -763,8 +755,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -810,8 +801,7 @@ public async Task Cannot_replace_for_missing_type_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -858,8 +848,7 @@ public async Task Cannot_replace_for_unknown_type_in_data() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -905,8 +894,7 @@ public async Task Cannot_replace_for_missing_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -954,8 +942,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1081,8 +1068,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1137,8 +1123,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 904e24e19c..30640ad32a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -549,8 +549,7 @@ public async Task Cannot_create_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -588,8 +587,7 @@ public async Task Cannot_create_for_missing_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -628,8 +626,7 @@ public async Task Cannot_create_for_unknown_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -667,8 +664,7 @@ public async Task Cannot_create_for_missing_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -722,6 +718,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("The requested resource 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]"); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -773,8 +770,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -814,8 +810,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -854,8 +849,7 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -902,8 +896,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -958,8 +951,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an object or 'null' in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1002,8 +994,7 @@ public async Task Cannot_create_for_missing_type_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1047,8 +1038,7 @@ public async Task Cannot_create_for_unknown_type_in_data() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1091,8 +1081,7 @@ public async Task Cannot_create_for_missing_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1137,8 +1126,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1192,6 +1180,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("A related resource 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]"); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -1243,8 +1232,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1296,8 +1284,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 44c5c94a9c..3e8e8f9740 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -340,8 +340,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -394,8 +393,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -450,8 +448,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -502,8 +499,7 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/tracks/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -555,8 +551,7 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -607,8 +602,7 @@ public async Task Cannot_replace_for_missing_ID_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -661,8 +655,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -798,8 +791,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 1100398738..f3892baa47 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -210,8 +210,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Unknown attribute found."); error.Detail.Should().Be("Attribute 'doesNotExist' does not exist on resource type 'musicTracks'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/doesNotExist"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -323,8 +322,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource type 'musicTracks'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/doesNotExist"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -634,8 +632,7 @@ public async Task Cannot_update_resource_for_href_element() error.Title.Should().Be("Failed to deserialize request body: The 'href' element is not supported."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/href"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -740,8 +737,7 @@ public async Task Cannot_update_resource_for_missing_type_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -789,8 +785,7 @@ public async Task Cannot_update_resource_for_missing_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -840,8 +835,7 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_ref() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -874,8 +868,7 @@ public async Task Cannot_update_resource_for_missing_data() error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -909,8 +902,7 @@ public async Task Cannot_update_resource_for_null_data() error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -963,8 +955,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1007,8 +998,7 @@ public async Task Cannot_update_resource_for_missing_type_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1051,8 +1041,7 @@ public async Task Cannot_update_resource_for_missing_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1097,8 +1086,7 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1147,8 +1135,7 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1200,8 +1187,7 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() error.Title.Should().Be("Failed to deserialize request body: Conflicting 'id' values found."); error.Detail.Should().Be($"Expected '{performerId1}' instead of '{performerId2}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1250,8 +1236,7 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da error.Title.Should().Be("Failed to deserialize request body: Conflicting 'lid' values found."); error.Detail.Should().Be("Expected 'local-1' instead of 'local-2'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/lid"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1302,8 +1287,7 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1354,8 +1338,7 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da error.Title.Should().Be("Failed to deserialize request body: The 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1399,8 +1382,7 @@ public async Task Cannot_update_resource_for_unknown_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1493,8 +1475,7 @@ public async Task Cannot_update_resource_for_incompatible_ID() error.Title.Should().Be("Failed to deserialize request body: Incompatible 'id' value found."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1544,8 +1525,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Attribute value cannot be assigned when updating resource."); error.Detail.Should().Be("The attribute 'createdAt' on resource type 'lyrics' cannot be assigned to."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/createdAt"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1595,8 +1575,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isArchived' on resource type 'playlists' is read-only."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/isArchived"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1646,8 +1625,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1697,8 +1675,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible attribute value found."); 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]/data/attributes/bornAt"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index e9dd973b36..1ec00d4b48 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -612,8 +612,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -673,8 +672,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an object or 'null' in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -722,8 +720,7 @@ public async Task Cannot_create_for_missing_type_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/track/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -772,8 +769,7 @@ public async Task Cannot_create_for_unknown_type_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -821,8 +817,7 @@ public async Task Cannot_create_for_missing_ID_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -872,8 +867,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -932,6 +926,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("A related resource 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]"); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -988,8 +983,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'."); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs index 51bb1c6f5c..5a02e47292 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs @@ -100,7 +100,7 @@ public async Task Cannot_convert_ActionResult_with_string_parameter_to_error_col 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."); + error.Detail.Should().Be("Data being returned must be null, errors or resources."); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/AlternateExceptionHandler.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/AlternateExceptionHandler.cs index d6af489f76..0ccbcf7947 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/AlternateExceptionHandler.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/AlternateExceptionHandler.cs @@ -24,7 +24,7 @@ protected override LogLevel GetLogLevel(Exception exception) return base.GetLogLevel(exception); } - protected override Document CreateErrorDocument(Exception exception) + protected override IReadOnlyList CreateErrorResponse(Exception exception) { if (exception is ConsumerArticleIsNoLongerAvailableException articleException) { @@ -34,7 +34,7 @@ protected override Document CreateErrorDocument(Exception exception) }; } - return base.CreateErrorDocument(exception); + return base.CreateErrorResponse(exception); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs index 601b7d84a8..a7dcd3329a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs @@ -110,12 +110,11 @@ public async Task Logs_and_produces_error_response_on_deserialization_failure() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be("Resource type '' does not exist."); + error.Meta["requestBody"].ToString().Should().Be(requestBody); IEnumerable stackTraceLines = ((JsonElement)error.Meta["stackTrace"]).EnumerateArray().Select(token => token.GetString()); stackTraceLines.Should().NotBeEmpty(); - responseDocument.Meta["requestBody"].ToString().Should().Be(requestBody); - loggerFactory.Logger.Messages.Should().BeEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index c8111c110b..25eff993b6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -287,8 +287,7 @@ public async Task Cannot_create_resource_with_unknown_attribute() error.Title.Should().Be("Failed to deserialize request body: Unknown attribute found."); error.Detail.Should().Be("Attribute 'doesNotExist' does not exist on resource type 'workItems'."); error.Source.Pointer.Should().Be("/data/attributes/doesNotExist"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -374,8 +373,7 @@ public async Task Cannot_create_resource_with_unknown_relationship() error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource type 'workItems'."); error.Source.Pointer.Should().Be("/data/relationships/doesNotExist"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -459,8 +457,7 @@ public async Task Cannot_create_resource_with_client_generated_ID() error.Title.Should().Be("Failed to deserialize request body: The use of client-generated IDs is disabled."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -481,9 +478,10 @@ public async Task Cannot_create_resource_for_missing_request_body() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Missing request body."); + error.Title.Should().Be("Failed to deserialize request body: Missing request body."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -513,8 +511,7 @@ public async Task Cannot_create_resource_for_missing_data() error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -541,8 +538,7 @@ public async Task Cannot_create_resource_for_null_data() error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of 'null'."); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -575,8 +571,7 @@ public async Task Cannot_create_resource_for_array_data() error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -608,8 +603,7 @@ public async Task Cannot_create_resource_for_missing_type() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -642,8 +636,7 @@ public async Task Cannot_create_resource_for_unknown_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -700,8 +693,7 @@ public async Task Cannot_create_on_resource_type_mismatch_between_url_and_body() error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'workItems'."); error.Source.Pointer.Should().Be("/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -735,8 +727,7 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() error.Title.Should().Be("Failed to deserialize request body: Attribute value cannot be assigned when creating resource."); error.Detail.Should().Be("The attribute 'isImportant' on resource type 'workItems' cannot be assigned to."); error.Source.Pointer.Should().Be("/data/attributes/isImportant"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -770,8 +761,7 @@ public async Task Cannot_create_resource_with_readonly_attribute() error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isDeprecated' on resource type 'workItemGroups' is read-only."); error.Source.Pointer.Should().Be("/data/attributes/isDeprecated"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -795,8 +785,7 @@ public async Task Cannot_create_resource_for_broken_JSON_request_body() error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("'{' is invalid after a property name."); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -830,8 +819,7 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() error.Title.Should().Be("Failed to deserialize request body: Incompatible attribute value found."); error.Detail.Should().Be("Failed to convert attribute 'dueAt' with value 'not-a-valid-time' of type 'String' to type 'Nullable'."); error.Source.Pointer.Should().Be("/data/attributes/dueAt"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs index f43aa5e2bf..181152a06d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs @@ -249,6 +249,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => 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."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index 7750d5dcdf..037ef0cb92 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -360,8 +360,7 @@ public async Task Cannot_create_for_missing_relationship_type() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -405,8 +404,7 @@ public async Task Cannot_create_for_unknown_relationship_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -449,8 +447,7 @@ public async Task Cannot_create_for_missing_relationship_ID() error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -549,8 +546,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'subscribers'."); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -652,8 +648,7 @@ public async Task Cannot_create_with_missing_data_in_OneToMany_relationship() error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/subscribers"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -690,8 +685,7 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/tags/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -730,8 +724,7 @@ public async Task Cannot_create_with_object_data_in_ManyToMany_relationship() error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/tags/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -778,8 +771,7 @@ public async Task Cannot_create_resource_with_local_ID() error.Title.Should().Be("Failed to deserialize request body: The 'lid' element is not supported at this endpoint."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/lid"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index bb0bdf367b..fd7472958c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -324,8 +324,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -377,8 +376,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an object or 'null' in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -418,8 +416,7 @@ public async Task Cannot_create_for_missing_relationship_type() error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -460,8 +457,7 @@ public async Task Cannot_create_for_unknown_relationship_type() error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -501,8 +497,7 @@ public async Task Cannot_create_for_missing_relationship_ID() error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -545,6 +540,7 @@ public async Task Cannot_create_with_unknown_relationship_ID() error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId}' in relationship 'assignee' does not exist."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -585,8 +581,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'assignee'."); error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -701,8 +696,7 @@ public async Task Cannot_create_resource_with_local_ID() error.Title.Should().Be("Failed to deserialize request body: The 'lid' element is not supported at this endpoint."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/lid"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs index b80327f7ad..4db422d5e8 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs @@ -76,6 +76,7 @@ public async Task Cannot_delete_unknown_resource() error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index 62254052ef..d827b58e4a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -61,8 +61,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this endpoint."); error.Detail.Should().Be("Relationship 'assignee' is not a to-many relationship."); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -205,9 +204,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Missing request body."); + error.Title.Should().Be("Failed to deserialize request body: Missing request body."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -248,8 +248,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -291,8 +290,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -333,8 +331,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -520,6 +517,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -561,6 +559,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("The requested relationship does not exist."); error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -603,8 +602,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'userAccounts' is incompatible with type 'workTags' of relationship 'tags'."); error.Source.Pointer.Should().Be("/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -722,8 +720,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -758,8 +755,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -796,8 +792,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index b3db05142f..0d97f78a2f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -76,8 +76,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted through this endpoint."); error.Detail.Should().Be("Relationship 'assignee' is not a to-many relationship."); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -321,8 +320,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Missing request body."); + error.Title.Should().Be("Failed to deserialize request body: Missing request body."); error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -364,8 +365,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -407,8 +407,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -449,8 +448,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -636,6 +634,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -677,6 +676,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("The requested relationship does not exist."); error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -719,8 +719,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'userAccounts' is incompatible with type 'workTags' of relationship 'tags'."); error.Source.Pointer.Should().Be("/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -840,8 +839,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -876,8 +874,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -914,8 +911,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index 78dd6ca290..ab4e3fc2bb 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -234,9 +234,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Missing request body."); + error.Title.Should().Be("Failed to deserialize request body: Missing request body."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -277,8 +278,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -320,8 +320,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -362,8 +361,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -546,6 +544,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -587,6 +586,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("The requested relationship does not exist."); error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -629,8 +629,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'userAccounts' is incompatible with type 'workTags' of relationship 'tags'."); error.Source.Pointer.Should().Be("/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -715,8 +714,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -751,8 +749,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -789,8 +786,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index 002c9bb902..a7cd13c53c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -267,9 +267,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Missing request body."); + error.Title.Should().Be("Failed to deserialize request body: Missing request body."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -304,8 +305,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -348,8 +348,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an object or 'null' in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -387,8 +386,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -427,8 +425,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -466,8 +463,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -508,6 +504,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId}' in relationship 'assignee' does not exist."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -581,6 +578,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -619,6 +617,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("The requested relationship does not exist."); error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -658,8 +657,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'assignee'."); error.Source.Pointer.Should().Be("/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index a09e232ca7..da348514cf 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -444,8 +444,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -498,8 +497,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -551,8 +549,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -698,8 +695,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'subscribers'."); error.Source.Pointer.Should().Be("/data/relationships/subscribers/data[0]/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -806,8 +802,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/subscribers"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -853,8 +848,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/tags/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -902,8 +896,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an array in 'data' element, instead of an object."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/tags/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index f87523d4a1..8f2fa5efab 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -128,8 +128,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Unknown attribute found."); error.Detail.Should().Be("Attribute 'doesNotExist' does not exist on resource type 'userAccounts'."); error.Source.Pointer.Should().Be("/data/attributes/doesNotExist"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -228,8 +227,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found."); error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource type 'userAccounts'."); error.Source.Pointer.Should().Be("/data/relationships/doesNotExist"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -658,9 +656,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Missing request body."); + error.Title.Should().Be("Failed to deserialize request body: Missing request body."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -698,8 +697,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -734,8 +732,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of 'null'."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -777,8 +774,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an object in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -816,8 +812,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -856,8 +851,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -895,8 +889,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -961,6 +954,7 @@ public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -999,8 +993,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'workItems'."); error.Source.Pointer.Should().Be("/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1039,8 +1032,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Conflicting 'id' values found."); error.Detail.Should().Be($"Expected '{existingWorkItems[1].StringId}' instead of '{existingWorkItems[0].StringId}'."); error.Source.Pointer.Should().Be("/data/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1083,8 +1075,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Attribute value cannot be assigned when updating resource."); error.Detail.Should().Be("The attribute 'isImportant' on resource type 'workItems' cannot be assigned to."); error.Source.Pointer.Should().Be("/data/attributes/isImportant"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1127,8 +1118,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isDeprecated' on resource type 'workItemGroups' is read-only."); error.Source.Pointer.Should().Be("/data/attributes/isDeprecated"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1160,8 +1150,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("Expected end of string, but instead reached end of data."); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1204,8 +1193,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/attributes/id"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1247,8 +1235,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert ID '{existingWorkItem.Id}' of type 'Number' to type 'String'."); error.Source.Should().BeNull(); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -1295,8 +1282,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible attribute value found."); error.Detail.Should().Match("Failed to convert attribute 'dueAt' with value '*start*end*' of type 'Object' to type 'Nullable'."); error.Source.Pointer.Should().Be("/data/attributes/dueAt"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index 046b6b8e19..7ee8e988bd 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -479,8 +479,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -534,8 +533,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Expected an object or 'null' in 'data' element, instead of an array."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -584,8 +582,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'type' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -635,8 +632,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Unknown resource type found."); error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -685,8 +681,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().Be("/data/relationships/assignee/data"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] @@ -738,6 +733,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId}' in relationship 'assignee' does not exist."); error.Source.Should().BeNull(); + error.Meta.Should().NotContainKey("requestBody"); } [Fact] @@ -787,8 +783,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found."); error.Detail.Should().Be("Type 'rgbColors' is incompatible with type 'userAccounts' of relationship 'assignee'."); error.Source.Pointer.Should().Be("/data/relationships/assignee/data/type"); - - responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); + error.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty(); } [Fact] diff --git a/test/UnitTests/Internal/ErrorDocumentTests.cs b/test/UnitTests/Internal/ErrorObjectTests.cs similarity index 79% rename from test/UnitTests/Internal/ErrorDocumentTests.cs rename to test/UnitTests/Internal/ErrorObjectTests.cs index e2426659b2..d60520df84 100644 --- a/test/UnitTests/Internal/ErrorDocumentTests.cs +++ b/test/UnitTests/Internal/ErrorObjectTests.cs @@ -6,7 +6,7 @@ namespace UnitTests.Internal { - public sealed class ErrorDocumentTests + public sealed class ErrorObjectTests { // @formatter:wrap_array_initializer_style wrap_if_long [Theory] @@ -18,13 +18,10 @@ public sealed class ErrorDocumentTests public void ErrorDocument_GetErrorStatusCode_IsCorrect(HttpStatusCode[] errorCodes, HttpStatusCode expected) { // Arrange - var document = new Document - { - Errors = errorCodes.Select(code => new ErrorObject(code)).ToList() - }; + ErrorObject[] errors = errorCodes.Select(code => new ErrorObject(code)).ToArray(); // Act - HttpStatusCode status = document.GetErrorStatusCode(); + HttpStatusCode status = ErrorObject.GetResponseStatusCode(errors); // Assert status.Should().Be(expected); diff --git a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs index 2058c79e85..6532b2085e 100644 --- a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs +++ b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs @@ -424,11 +424,6 @@ public void SerializeError_Error_CanSerialize() Detail = "detail" }; - var errorDocument = new Document - { - Errors = error.AsList() - }; - string expectedJson = JsonSerializer.Serialize(new { errors = new[] @@ -446,7 +441,7 @@ public void SerializeError_Error_CanSerialize() ResponseSerializer serializer = GetResponseSerializer(); // Act - string result = serializer.Serialize(errorDocument); + string result = serializer.Serialize(error); // Assert Assert.Equal(expectedJson, result); From 65b1f03ea9d43b5292d6ab9550c33da8567a6f01 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Sat, 2 Oct 2021 13:56:09 +0200 Subject: [PATCH 24/49] Enhanced existing tests: Assert on resource type when `included` contains mixed types --- .../QueryStrings/Filtering/FilterDepthTests.cs | 10 ++++++++++ .../Pagination/PaginationWithTotalCountTests.cs | 6 ++++++ .../IntegrationTests/QueryStrings/Sorting/SortTests.cs | 10 ++++++++++ .../SparseFieldSets/SparseFieldSetTests.cs | 6 ++++++ 4 files changed, 32 insertions(+) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs index 4d13fd69c1..807f104a15 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs @@ -415,7 +415,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Included.Should().HaveCount(2); + + responseDocument.Included[0].Type.Should().Be("webAccounts"); responseDocument.Included[0].Id.Should().Be(blog.Owner.StringId); + + responseDocument.Included[1].Type.Should().Be("blogPosts"); responseDocument.Included[1].Id.Should().Be(blog.Owner.Posts[1].StringId); } @@ -530,8 +534,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); responseDocument.Included.Should().HaveCount(3); + + responseDocument.Included[0].Type.Should().Be("webAccounts"); responseDocument.Included[0].Id.Should().Be(blogs[1].Owner.StringId); + + responseDocument.Included[1].Type.Should().Be("blogPosts"); responseDocument.Included[1].Id.Should().Be(blogs[1].Owner.Posts[1].StringId); + + responseDocument.Included[2].Type.Should().Be("comments"); responseDocument.Included[2].Id.Should().Be(blogs[1].Owner.Posts[1].Comments.ElementAt(1).StringId); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs index 8d87adfc7d..5fa24d4e7b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs @@ -368,8 +368,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); responseDocument.Included.Should().HaveCount(3); + + responseDocument.Included[0].Type.Should().Be("webAccounts"); responseDocument.Included[0].Id.Should().Be(blogs[1].Owner.StringId); + + responseDocument.Included[1].Type.Should().Be("blogPosts"); responseDocument.Included[1].Id.Should().Be(blogs[1].Owner.Posts[1].StringId); + + responseDocument.Included[2].Type.Should().Be("comments"); responseDocument.Included[2].Id.Should().Be(blogs[1].Owner.Posts[1].Comments.ElementAt(1).StringId); string linkPrefix = $"{HostPrefix}/blogs?include=owner.posts.comments"; diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs index 43ceb3fdf9..c6ec812da0 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs @@ -433,10 +433,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Data.ManyValue[1].Id.Should().Be(blogs[0].StringId); responseDocument.Included.Should().HaveCount(5); + + responseDocument.Included[0].Type.Should().Be("webAccounts"); responseDocument.Included[0].Id.Should().Be(blogs[1].Owner.StringId); + + responseDocument.Included[1].Type.Should().Be("blogPosts"); responseDocument.Included[1].Id.Should().Be(blogs[1].Owner.Posts[1].StringId); + + responseDocument.Included[2].Type.Should().Be("comments"); responseDocument.Included[2].Id.Should().Be(blogs[1].Owner.Posts[1].Comments.ElementAt(1).StringId); + + responseDocument.Included[3].Type.Should().Be("comments"); responseDocument.Included[3].Id.Should().Be(blogs[1].Owner.Posts[1].Comments.ElementAt(0).StringId); + + responseDocument.Included[4].Type.Should().Be("blogPosts"); responseDocument.Included[4].Id.Should().Be(blogs[1].Owner.Posts[0].StringId); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs index a8681a1403..1ce5469a7b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs @@ -445,6 +445,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("blogs"); responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); responseDocument.Data.SingleValue.Attributes["title"].Should().Be(blog.Title); @@ -452,12 +453,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included.Should().HaveCount(2); + responseDocument.Included[0].Type.Should().Be("webAccounts"); responseDocument.Included[0].Id.Should().Be(blog.Owner.StringId); responseDocument.Included[0].Attributes.Should().HaveCount(2); responseDocument.Included[0].Attributes["userName"].Should().Be(blog.Owner.UserName); responseDocument.Included[0].Attributes["displayName"].Should().Be(blog.Owner.DisplayName); responseDocument.Included[0].Relationships.Should().BeNull(); + responseDocument.Included[1].Type.Should().Be("blogPosts"); responseDocument.Included[1].Id.Should().Be(blog.Owner.Posts[0].StringId); responseDocument.Included[1].Attributes.Should().HaveCount(1); responseDocument.Included[1].Attributes["caption"].Should().Be(blog.Owner.Posts[0].Caption); @@ -503,6 +506,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("blogs"); responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); responseDocument.Data.SingleValue.Attributes["title"].Should().Be(blog.Title); @@ -513,6 +517,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included.Should().HaveCount(2); + responseDocument.Included[0].Type.Should().Be("webAccounts"); 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); @@ -522,6 +527,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Relationships["posts"].Links.Self.Should().NotBeNull(); responseDocument.Included[0].Relationships["posts"].Links.Related.Should().NotBeNull(); + responseDocument.Included[1].Type.Should().Be("blogPosts"); 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); From 5198265e30d1621f5febad86b94f9acc2a0d6cad Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 4 Oct 2021 14:17:38 +0200 Subject: [PATCH 25/49] Fixed the number of resource definition callbacks for sparse fieldsets --- .../JsonApiSerializerBenchmarks.cs | 33 +++++++-------- .../AtomicOperations/OperationsProcessor.cs | 10 ++++- .../JsonApiApplicationBuilder.cs | 1 + .../Queries/Internal/ISparseFieldSetCache.cs | 40 +++++++++++++++++++ .../Queries/Internal/QueryLayerComposer.cs | 7 ++-- .../Queries/Internal/SparseFieldSetCache.cs | 12 +++--- .../AtomicOperationsResponseSerializer.cs | 7 +++- .../Building/IncludedResourceObjectBuilder.cs | 7 ++-- .../Building/ResponseResourceObjectBuilder.cs | 7 ++-- .../Serialization/FieldsToSerialize.cs | 13 ++---- .../Serialization/IFieldsToSerialize.cs | 5 --- .../Serialization/ResponseSerializer.cs | 8 +++- .../Reading/ResourceDefinitionReadTests.cs | 7 ---- .../Serialization/SerializerTestsSetup.cs | 15 ++++--- .../IncludedResourceObjectBuilderTests.cs | 8 +++- 15 files changed, 113 insertions(+), 67 deletions(-) create mode 100644 src/JsonApiDotNetCore/Queries/Internal/ISparseFieldSetCache.cs diff --git a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs index 0fa58c272e..9c5b9f4821 100644 --- a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.Internal; using JsonApiDotNetCore.QueryStrings.Internal; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization; @@ -27,33 +28,29 @@ public class JsonApiSerializerBenchmarks public JsonApiSerializerBenchmarks() { var options = new JsonApiOptions(); + var request = new JsonApiRequest(); + IResourceGraph resourceGraph = _dependencyFactory.CreateResourceGraph(options); - IFieldsToSerialize fieldsToSerialize = CreateFieldsToSerialize(resourceGraph); + + var constraintProviders = new IQueryConstraintProvider[] + { + new SparseFieldSetQueryStringParameterReader(request, resourceGraph) + }; + + IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock().Object; + var sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); IMetaBuilder metaBuilder = new Mock().Object; ILinkBuilder linkBuilder = new Mock().Object; IIncludedResourceObjectBuilder includeBuilder = new Mock().Object; - var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, options); + IFieldsToSerialize fieldsToSerialize = + new FieldsToSerialize(resourceGraph, constraintProviders, resourceDefinitionAccessor, request, sparseFieldSetCache); - IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock().Object; + var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, options); _jsonApiSerializer = new ResponseSerializer(metaBuilder, linkBuilder, includeBuilder, fieldsToSerialize, resourceObjectBuilder, - resourceDefinitionAccessor, options); - } - - private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph) - { - var request = new JsonApiRequest(); - - var constraintProviders = new IQueryConstraintProvider[] - { - new SparseFieldSetQueryStringParameterReader(request, resourceGraph) - }; - - IResourceDefinitionAccessor accessor = new Mock().Object; - - return new FieldsToSerialize(resourceGraph, constraintProviders, accessor, request); + resourceDefinitionAccessor, sparseFieldSetCache, options); } [Benchmark] diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs index 284abe7995..0c13dc3f49 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; -using System.Net; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Queries.Internal; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; @@ -22,10 +22,12 @@ public class OperationsProcessor : IOperationsProcessor private readonly IResourceGraph _resourceGraph; private readonly IJsonApiRequest _request; private readonly ITargetedFields _targetedFields; + private readonly ISparseFieldSetCache _sparseFieldSetCache; private readonly LocalIdValidator _localIdValidator; public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccessor, IOperationsTransactionFactory operationsTransactionFactory, - ILocalIdTracker localIdTracker, IResourceGraph resourceGraph, IJsonApiRequest request, ITargetedFields targetedFields) + ILocalIdTracker localIdTracker, IResourceGraph resourceGraph, IJsonApiRequest request, ITargetedFields targetedFields, + ISparseFieldSetCache sparseFieldSetCache) { ArgumentGuard.NotNull(operationProcessorAccessor, nameof(operationProcessorAccessor)); ArgumentGuard.NotNull(operationsTransactionFactory, nameof(operationsTransactionFactory)); @@ -33,6 +35,7 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); + ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); _operationProcessorAccessor = operationProcessorAccessor; _operationsTransactionFactory = operationsTransactionFactory; @@ -40,6 +43,7 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso _resourceGraph = resourceGraph; _request = request; _targetedFields = targetedFields; + _sparseFieldSetCache = sparseFieldSetCache; _localIdValidator = new LocalIdValidator(_localIdTracker, _resourceGraph); } @@ -67,6 +71,8 @@ public virtual async Task> ProcessAsync(IList dbContextTypes) _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); _services.AddScoped(); _services.AddScoped(); + _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); } diff --git a/src/JsonApiDotNetCore/Queries/Internal/ISparseFieldSetCache.cs b/src/JsonApiDotNetCore/Queries/Internal/ISparseFieldSetCache.cs new file mode 100644 index 0000000000..5571474e0f --- /dev/null +++ b/src/JsonApiDotNetCore/Queries/Internal/ISparseFieldSetCache.cs @@ -0,0 +1,40 @@ +using System.Collections.Immutable; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Queries.Internal +{ + /// + /// Takes sparse fieldsets from s and invokes + /// on them. + /// + /// + /// This cache ensures that for each request (or operation per request), the resource definition callback is executed only twice per resource type. The + /// first invocation is used to obtain the fields to retrieve from the underlying data store, while the second invocation is used to determine which + /// fields to write to the response body. + /// + public interface ISparseFieldSetCache + { + /// + /// Gets the set of sparse fields to retrieve from the underlying data store. Returns an empty set to retrieve all fields. + /// + IImmutableSet GetSparseFieldSetForQuery(ResourceContext resourceContext); + + /// + /// Gets the set of attributes to retrieve from the underlying data store for relationship endpoints. This always returns 'id', along with any additional + /// attributes from resource definition callback. + /// + IImmutableSet GetIdAttributeSetForRelationshipQuery(ResourceContext resourceContext); + + /// + /// Gets the evaluated set of sparse fields to serialize into the response body. + /// + IImmutableSet GetSparseFieldSetForSerializer(ResourceContext resourceContext); + + /// + /// Resets the cached results from resource definition callbacks. + /// + void Reset(); + } +} diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index 6bc933cfc0..9bfe10d6f3 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -23,11 +23,11 @@ public class QueryLayerComposer : IQueryLayerComposer private readonly IPaginationContext _paginationContext; private readonly ITargetedFields _targetedFields; private readonly IEvaluatedIncludeCache _evaluatedIncludeCache; - private readonly SparseFieldSetCache _sparseFieldSetCache; + private readonly ISparseFieldSetCache _sparseFieldSetCache; public QueryLayerComposer(IEnumerable constraintProviders, IResourceGraph resourceGraph, IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiOptions options, IPaginationContext paginationContext, - ITargetedFields targetedFields, IEvaluatedIncludeCache evaluatedIncludeCache) + ITargetedFields targetedFields, IEvaluatedIncludeCache evaluatedIncludeCache, ISparseFieldSetCache sparseFieldSetCache) { ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); @@ -36,6 +36,7 @@ public QueryLayerComposer(IEnumerable constraintProvid ArgumentGuard.NotNull(paginationContext, nameof(paginationContext)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache)); + ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); _constraintProviders = constraintProviders; _resourceGraph = resourceGraph; @@ -44,7 +45,7 @@ public QueryLayerComposer(IEnumerable constraintProvid _paginationContext = paginationContext; _targetedFields = targetedFields; _evaluatedIncludeCache = evaluatedIncludeCache; - _sparseFieldSetCache = new SparseFieldSetCache(_constraintProviders, resourceDefinitionAccessor); + _sparseFieldSetCache = sparseFieldSetCache; } /// diff --git a/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs b/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs index 573b19e4a4..bf2f09fbb2 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; @@ -10,12 +9,8 @@ namespace JsonApiDotNetCore.Queries.Internal { - /// - /// Takes sparse fieldsets from s and invokes - /// on them. - /// - [PublicAPI] - public sealed class SparseFieldSetCache + /// + public sealed class SparseFieldSetCache : ISparseFieldSetCache { private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly Lazy>> _lazySourceTable; @@ -73,6 +68,7 @@ private static void AddSparseFieldsToSet(IImmutableSet s } } + /// public IImmutableSet GetSparseFieldSetForQuery(ResourceContext resourceContext) { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); @@ -95,6 +91,7 @@ public IImmutableSet GetSparseFieldSetForQuery(ResourceC return _visitedTable[resourceContext]; } + /// public IImmutableSet GetIdAttributeSetForRelationshipQuery(ResourceContext resourceContext) { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); @@ -113,6 +110,7 @@ public IImmutableSet GetIdAttributeSetForRelationshipQuery(Resour return outputAttributes; } + /// public IImmutableSet GetSparseFieldSetForSerializer(ResourceContext resourceContext) { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); diff --git a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs index 651ce87033..e4f11541ab 100644 --- a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs @@ -23,6 +23,7 @@ public sealed class AtomicOperationsResponseSerializer : BaseSerializer, IJsonAp private readonly IFieldsToSerialize _fieldsToSerialize; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly IEvaluatedIncludeCache _evaluatedIncludeCache; + private readonly ISparseFieldSetCache _sparseFieldSetCache; private readonly IJsonApiRequest _request; private readonly IJsonApiOptions _options; @@ -31,7 +32,7 @@ public sealed class AtomicOperationsResponseSerializer : BaseSerializer, IJsonAp public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectBuilder, IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, IFieldsToSerialize fieldsToSerialize, IResourceDefinitionAccessor resourceDefinitionAccessor, IEvaluatedIncludeCache evaluatedIncludeCache, - IJsonApiRequest request, IJsonApiOptions options) + ISparseFieldSetCache sparseFieldSetCache, IJsonApiRequest request, IJsonApiOptions options) : base(resourceObjectBuilder) { ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder)); @@ -39,6 +40,7 @@ public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectB ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache)); + ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(options, nameof(options)); @@ -47,6 +49,7 @@ public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectB _fieldsToSerialize = fieldsToSerialize; _resourceDefinitionAccessor = resourceDefinitionAccessor; _evaluatedIncludeCache = evaluatedIncludeCache; + _sparseFieldSetCache = sparseFieldSetCache; _request = request; _options = options; } @@ -117,7 +120,7 @@ private AtomicResultObject SerializeOperation(OperationContainer operation) if (operation != null) { _request.CopyFrom(operation.Request); - _fieldsToSerialize.ResetCache(); + _sparseFieldSetCache.Reset(); _evaluatedIncludeCache.Set(null); _resourceDefinitionAccessor.OnSerialize(operation.Resource); diff --git a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs index 299c270f91..5dad7eaad9 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs @@ -22,11 +22,11 @@ public class IncludedResourceObjectBuilder : ResourceObjectBuilder, IIncludedRes private readonly ILinkBuilder _linkBuilder; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly IRequestQueryStringAccessor _queryStringAccessor; - private readonly SparseFieldSetCache _sparseFieldSetCache; + private readonly ISparseFieldSetCache _sparseFieldSetCache; public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILinkBuilder linkBuilder, IResourceGraph resourceGraph, IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, - IRequestQueryStringAccessor queryStringAccessor, IJsonApiOptions options) + IRequestQueryStringAccessor queryStringAccessor, IJsonApiOptions options, ISparseFieldSetCache sparseFieldSetCache) : base(resourceGraph, options) { ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); @@ -34,13 +34,14 @@ public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILink ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); ArgumentGuard.NotNull(queryStringAccessor, nameof(queryStringAccessor)); + ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); _included = new HashSet(ResourceIdentityComparer.Instance); _fieldsToSerialize = fieldsToSerialize; _linkBuilder = linkBuilder; _resourceDefinitionAccessor = resourceDefinitionAccessor; _queryStringAccessor = queryStringAccessor; - _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); + _sparseFieldSetCache = sparseFieldSetCache; } /// diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs index 7138b6a12b..2f5e49fad3 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs @@ -21,13 +21,13 @@ public class ResponseResourceObjectBuilder : ResourceObjectBuilder private readonly IIncludedResourceObjectBuilder _includedBuilder; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly IEvaluatedIncludeCache _evaluatedIncludeCache; - private readonly SparseFieldSetCache _sparseFieldSetCache; + private readonly ISparseFieldSetCache _sparseFieldSetCache; private RelationshipAttribute _requestRelationship; public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, IEnumerable constraintProviders, IResourceGraph resourceGraph, IResourceDefinitionAccessor resourceDefinitionAccessor, - IJsonApiOptions options, IEvaluatedIncludeCache evaluatedIncludeCache) + IJsonApiOptions options, IEvaluatedIncludeCache evaluatedIncludeCache, ISparseFieldSetCache sparseFieldSetCache) : base(resourceGraph, options) { ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); @@ -35,12 +35,13 @@ public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IIncludedResource ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache)); + ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); _linkBuilder = linkBuilder; _includedBuilder = includedBuilder; _resourceDefinitionAccessor = resourceDefinitionAccessor; _evaluatedIncludeCache = evaluatedIncludeCache; - _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); + _sparseFieldSetCache = sparseFieldSetCache; } public RelationshipObject Build(IIdentifiable resource, RelationshipAttribute requestRelationship) diff --git a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs index e19ff666d3..988f0dc8b1 100644 --- a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs +++ b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs @@ -18,20 +18,21 @@ public class FieldsToSerialize : IFieldsToSerialize { private readonly IResourceGraph _resourceGraph; private readonly IJsonApiRequest _request; - private readonly SparseFieldSetCache _sparseFieldSetCache; + private readonly ISparseFieldSetCache _sparseFieldSetCache; /// public bool ShouldSerialize => _request.Kind != EndpointKind.Relationship; public FieldsToSerialize(IResourceGraph resourceGraph, IEnumerable constraintProviders, - IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiRequest request) + IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiRequest request, ISparseFieldSetCache sparseFieldSetCache) { ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); _resourceGraph = resourceGraph; _request = request; - _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); + _sparseFieldSetCache = sparseFieldSetCache; } /// @@ -79,11 +80,5 @@ public IReadOnlyCollection GetRelationships(Type resource ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); return resourceContext.Relationships; } - - /// - public void ResetCache() - { - _sparseFieldSetCache.Reset(); - } } } diff --git a/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs index 682301b040..a55c923a56 100644 --- a/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs +++ b/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs @@ -24,10 +24,5 @@ public interface IFieldsToSerialize /// Gets the collection of relationships that are to be serialized for resources of type . /// IReadOnlyCollection GetRelationships(Type resourceType); - - /// - /// Clears internal caches. - /// - void ResetCache(); } } diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs index 567d14f5e6..b629c8f1bb 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs @@ -4,6 +4,7 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Queries.Internal; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; @@ -31,6 +32,7 @@ public class ResponseSerializer : BaseSerializer, IJsonApiSerializer private readonly IIncludedResourceObjectBuilder _includedBuilder; private readonly IFieldsToSerialize _fieldsToSerialize; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; + private readonly ISparseFieldSetCache _sparseFieldSetCache; private readonly IJsonApiOptions _options; private readonly Type _primaryResourceType; @@ -39,7 +41,7 @@ public class ResponseSerializer : BaseSerializer, IJsonApiSerializer public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, IFieldsToSerialize fieldsToSerialize, IResourceObjectBuilder resourceObjectBuilder, IResourceDefinitionAccessor resourceDefinitionAccessor, - IJsonApiOptions options) + ISparseFieldSetCache sparseFieldSetCache, IJsonApiOptions options) : base(resourceObjectBuilder) { ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder)); @@ -47,6 +49,7 @@ public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, II ArgumentGuard.NotNull(includedBuilder, nameof(includedBuilder)); ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); + ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); ArgumentGuard.NotNull(options, nameof(options)); _metaBuilder = metaBuilder; @@ -54,6 +57,7 @@ public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, II _includedBuilder = includedBuilder; _fieldsToSerialize = fieldsToSerialize; _resourceDefinitionAccessor = resourceDefinitionAccessor; + _sparseFieldSetCache = sparseFieldSetCache; _options = options; _primaryResourceType = typeof(TResource); } @@ -61,6 +65,8 @@ public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, II /// public string Serialize(object content) { + _sparseFieldSetCache.Reset(); + if (content == null || content is IIdentifiable) { return SerializeSingle((IIdentifiable)content); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs index 675d562f5f..0018ece30e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs @@ -261,7 +261,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), - (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) }, options => options.WithStrictOrdering()); } @@ -308,7 +307,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), - (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) }, options => options.WithStrictOrdering()); } @@ -343,7 +341,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), - (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) }, options => options.WithStrictOrdering()); } @@ -381,7 +378,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), - (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) }, options => options.WithStrictOrdering()); } @@ -420,7 +416,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), - (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) }, options => options.WithStrictOrdering()); } @@ -458,7 +453,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), - (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) }, options => options.WithStrictOrdering()); } @@ -496,7 +490,6 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyPagination), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySort), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), - (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet), (typeof(Star), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplySparseFieldSet) }, options => options.WithStrictOrdering()); } diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index ccc5dae8e9..ba82d9382a 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -62,12 +62,14 @@ protected ResponseSerializer GetResponseSerializer(IEnumerable(), resourceDefinitionAccessor); var options = new JsonApiOptions(); var resourceObjectBuilder = new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, resourceDefinitionAccessor, - options, evaluatedIncludeCache); + options, evaluatedIncludeCache, sparseFieldSetCache); - return new ResponseSerializer(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, resourceDefinitionAccessor, options); + return new ResponseSerializer(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, resourceDefinitionAccessor, + sparseFieldSetCache, options); } protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(IEnumerable> inclusionChains = null, @@ -79,10 +81,12 @@ protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(IEnumer IEnumerable includeConstraints = GetIncludeConstraints(inclusionChainArray); IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(inclusionChains != null); IEvaluatedIncludeCache evaluatedIncludeCache = GetEvaluatedIncludeCache(inclusionChainArray); + IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor(); + var sparseFieldSetCache = new SparseFieldSetCache(Enumerable.Empty(), resourceDefinitionAccessor); var options = new JsonApiOptions(); - return new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, GetResourceDefinitionAccessor(), options, - evaluatedIncludeCache); + return new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, resourceDefinitionAccessor, options, + evaluatedIncludeCache, sparseFieldSetCache); } private IIncludedResourceObjectBuilder GetIncludedBuilder(bool hasIncludeQueryString) @@ -91,10 +95,11 @@ private IIncludedResourceObjectBuilder GetIncludedBuilder(bool hasIncludeQuerySt ILinkBuilder linkBuilder = GetLinkBuilder(); IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor(); IRequestQueryStringAccessor queryStringAccessor = new FakeRequestQueryStringAccessor(hasIncludeQueryString ? "include=" : null); + var sparseFieldSetCache = new SparseFieldSetCache(Enumerable.Empty(), resourceDefinitionAccessor); var options = new JsonApiOptions(); return new IncludedResourceObjectBuilder(fieldsToSerialize, linkBuilder, ResourceGraph, Enumerable.Empty(), - resourceDefinitionAccessor, queryStringAccessor, options); + resourceDefinitionAccessor, queryStringAccessor, options, sparseFieldSetCache); } private IResourceDefinitionAccessor GetResourceDefinitionAccessor() diff --git a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs index ee4fb88492..d3946f15a0 100644 --- a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.Internal; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization; @@ -182,9 +184,11 @@ private IncludedResourceObjectBuilder GetBuilder() IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock().Object; var queryStringAccessor = new FakeRequestQueryStringAccessor(); var options = new JsonApiOptions(); + IEnumerable constraintProviders = Array.Empty(); + var sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); - return new IncludedResourceObjectBuilder(fields, links, ResourceGraph, Enumerable.Empty(), resourceDefinitionAccessor, - queryStringAccessor, options); + return new IncludedResourceObjectBuilder(fields, links, ResourceGraph, constraintProviders, resourceDefinitionAccessor, queryStringAccessor, + options, sparseFieldSetCache); } private sealed class AuthorChainInstances From 64cc4e2cf2bf318ba8f4a85dc0fd5f2c715344bd Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 4 Oct 2021 14:32:40 +0200 Subject: [PATCH 26/49] Refactor: removed OperationContainer.Kind because IJsonApiRequest.WriteOperation is now populated consistently; applied c# pattern usage --- src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs | 4 ++-- .../AtomicOperations/OperationProcessorAccessor.cs | 2 +- .../AtomicOperations/OperationsProcessor.cs | 2 +- .../Configuration/JsonApiValidationFilter.cs | 2 +- .../Controllers/BaseJsonApiOperationsController.cs | 2 +- .../Internal/QueryableBuilding/QueryClauseBuilder.cs | 2 +- .../Internal/PaginationQueryStringParameterReader.cs | 2 +- .../Repositories/EntityFrameworkCoreRepository.cs | 2 +- .../Resources/Internal/RuntimeTypeConverter.cs | 3 +-- src/JsonApiDotNetCore/Resources/OperationContainer.cs | 6 ++---- src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs | 2 +- .../RequestAdapters/AtomicOperationObjectAdapter.cs | 4 ++-- .../RequestAdapters/ResourceIdentityAdapter.cs | 3 +-- src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs | 2 +- .../Controllers/CreateMusicTrackOperationsController.cs | 2 +- 15 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs index d880ab7b42..736c91ea98 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs @@ -57,7 +57,7 @@ public void Validate(IEnumerable operations) private void ValidateOperation(OperationContainer operation) { - if (operation.Kind == WriteOperationKind.CreateResource) + if (operation.Request.WriteOperation == WriteOperationKind.CreateResource) { DeclareLocalId(operation.Resource); } @@ -71,7 +71,7 @@ private void ValidateOperation(OperationContainer operation) AssertLocalIdIsAssigned(secondaryResource); } - if (operation.Kind == WriteOperationKind.CreateResource) + if (operation.Request.WriteOperation == WriteOperationKind.CreateResource) { AssignLocalId(operation); } diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs index a71fa906cd..516d40b90c 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs @@ -37,7 +37,7 @@ public Task ProcessAsync(OperationContainer operation, Cance protected virtual IOperationProcessor ResolveProcessor(OperationContainer operation) { - Type processorInterface = GetProcessorInterface(operation.Kind); + Type processorInterface = GetProcessorInterface(operation.Request.WriteOperation.GetValueOrDefault()); ResourceContext resourceContext = _resourceGraph.GetResourceContext(operation.Resource.GetType()); Type processorType = processorInterface.MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs index 0c13dc3f49..4613aae75b 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs @@ -115,7 +115,7 @@ protected virtual async Task ProcessOperationAsync(Operation protected void TrackLocalIdsForOperation(OperationContainer operation) { - if (operation.Kind == WriteOperationKind.CreateResource) + if (operation.Request.WriteOperation == WriteOperationKind.CreateResource) { DeclareLocalId(operation.Resource); } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs index 1a661aaf1e..7cefd4e120 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs @@ -57,7 +57,7 @@ private static bool IsId(string key) private static bool IsAtPrimaryEndpoint(IJsonApiRequest request) { - return request.Kind == EndpointKind.Primary || request.Kind == EndpointKind.AtomicOperations; + return request.Kind is EndpointKind.Primary or EndpointKind.AtomicOperations; } private static bool IsFieldTargeted(ValidationEntry entry, ITargetedFields targetedFields) diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs index 4fb78e8515..5354522a55 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs @@ -133,7 +133,7 @@ protected virtual void ValidateModelState(IEnumerable operat foreach (OperationContainer operation in operations) { - if (operation.Kind == WriteOperationKind.CreateResource || operation.Kind == WriteOperationKind.UpdateResource) + if (operation.Request.WriteOperation is WriteOperationKind.CreateResource or WriteOperationKind.UpdateResource) { _targetedFields.CopyFrom(operation.TargetedFields); _request.CopyFrom(operation.Request); diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs index 2a51b561c9..1defb30729 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs @@ -49,7 +49,7 @@ private static Expression TryGetCollectionCount(Expression collectionExpression) foreach (PropertyInfo property in properties) { - if (property.Name == "Count" || property.Name == "Length") + if (property.Name is "Count" or "Length") { return Expression.Property(collectionExpression, property); } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs index 47c6ec595e..aa117847ea 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs @@ -45,7 +45,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr /// public virtual bool CanRead(string parameterName) { - return parameterName == PageSizeParameterName || parameterName == PageNumberParameterName; + return parameterName is PageSizeParameterName or PageNumberParameterName; } /// diff --git a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs index c6ba15f10d..a929856fa1 100644 --- a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs @@ -570,7 +570,7 @@ protected virtual async Task SaveChangesAsync(CancellationToken cancellationToke await _dbContext.SaveChangesAsync(cancellationToken); } - catch (Exception exception) when (exception is DbUpdateException || exception is InvalidOperationException) + catch (Exception exception) when (exception is DbUpdateException or InvalidOperationException) { if (_dbContext.Database.CurrentTransaction != null) { diff --git a/src/JsonApiDotNetCore/Resources/Internal/RuntimeTypeConverter.cs b/src/JsonApiDotNetCore/Resources/Internal/RuntimeTypeConverter.cs index f7aaad9192..826b375bd3 100644 --- a/src/JsonApiDotNetCore/Resources/Internal/RuntimeTypeConverter.cs +++ b/src/JsonApiDotNetCore/Resources/Internal/RuntimeTypeConverter.cs @@ -71,8 +71,7 @@ public static object ConvertType(object value, Type type) // https://bradwilson.typepad.com/blog/2008/07/creating-nullab.html return Convert.ChangeType(stringValue, nonNullableType); } - catch (Exception exception) when (exception is FormatException || exception is OverflowException || exception is InvalidCastException || - exception is ArgumentException) + catch (Exception exception) when (exception is FormatException or OverflowException or InvalidCastException or ArgumentException) { string runtimeTypeName = runtimeType.GetFriendlyTypeName(); string targetTypeName = type.GetFriendlyTypeName(); diff --git a/src/JsonApiDotNetCore/Resources/OperationContainer.cs b/src/JsonApiDotNetCore/Resources/OperationContainer.cs index d5350a34dc..ada612de59 100644 --- a/src/JsonApiDotNetCore/Resources/OperationContainer.cs +++ b/src/JsonApiDotNetCore/Resources/OperationContainer.cs @@ -13,18 +13,16 @@ public sealed class OperationContainer { private static readonly CollectionConverter CollectionConverter = new(); - public WriteOperationKind Kind { get; } public IIdentifiable Resource { get; } public ITargetedFields TargetedFields { get; } public IJsonApiRequest Request { get; } - public OperationContainer(WriteOperationKind kind, IIdentifiable resource, ITargetedFields targetedFields, IJsonApiRequest request) + public OperationContainer(IIdentifiable resource, ITargetedFields targetedFields, IJsonApiRequest request) { ArgumentGuard.NotNull(resource, nameof(resource)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); ArgumentGuard.NotNull(request, nameof(request)); - Kind = kind; Resource = resource; TargetedFields = targetedFields; Request = request; @@ -39,7 +37,7 @@ public OperationContainer WithResource(IIdentifiable resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - return new OperationContainer(Kind, resource, TargetedFields, Request); + return new OperationContainer(resource, TargetedFields, Request); } public ISet GetSecondaryResources() diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs index 59c6fa4cec..d31739ec9c 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs @@ -105,7 +105,7 @@ private string SerializeResponse(object contextObject, HttpStatusCode statusCode throw new UnsuccessfulActionResultException(statusCode); } - if (statusCode == HttpStatusCode.NoContent || statusCode == HttpStatusCode.ResetContent || statusCode == HttpStatusCode.NotModified) + if (statusCode is HttpStatusCode.NoContent or HttpStatusCode.ResetContent or HttpStatusCode.NotModified) { // Prevent exception from Kestrel server, caused by writing data:null json response. return null; diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs index 90d106f510..5b9dcd3202 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs @@ -49,12 +49,12 @@ public OperationContainer Convert(AtomicOperationObject atomicOperationObject, R (ResourceIdentityRequirements requirements, IIdentifiable primaryResource) = ConvertRef(atomicOperationObject, state); - if (writeOperation == WriteOperationKind.CreateResource || writeOperation == WriteOperationKind.UpdateResource) + if (writeOperation is WriteOperationKind.CreateResource or WriteOperationKind.UpdateResource) { primaryResource = _resourceDataInOperationsRequestAdapter.Convert(atomicOperationObject.Data, requirements, state); } - return new OperationContainer(writeOperation, primaryResource, state.WritableTargetedFields, state.Request); + return new OperationContainer(primaryResource, state.WritableTargetedFields, state.Request); } private static void AssertNoHref(AtomicOperationObject atomicOperationObject, RequestAdapterState state) diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs index 2b66a51394..05431d6309 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs @@ -206,8 +206,7 @@ protected static void AssertIsKnownRelationship(RelationshipAttribute relationsh protected internal static void AssertToManyInAddOrRemoveRelationship(RelationshipAttribute relationship, RequestAdapterState state) { - bool requireToManyRelationship = state.Request.WriteOperation == WriteOperationKind.AddToRelationship || - state.Request.WriteOperation == WriteOperationKind.RemoveFromRelationship; + bool requireToManyRelationship = state.Request.WriteOperation is WriteOperationKind.AddToRelationship or WriteOperationKind.RemoveFromRelationship; if (requireToManyRelationship && relationship is not HasManyAttribute) { diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs index b629c8f1bb..02ef21f177 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs @@ -67,7 +67,7 @@ public string Serialize(object content) { _sparseFieldSetCache.Reset(); - if (content == null || content is IIdentifiable) + if (content is null or IIdentifiable) { return SerializeSingle((IIdentifiable)content); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs index 5806456a9c..c0b8319fac 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs @@ -38,7 +38,7 @@ private static void AssertOnlyCreatingMusicTracks(IEnumerable Date: Mon, 4 Oct 2021 15:14:17 +0200 Subject: [PATCH 27/49] Added test to capture the current behavior to return data:null for void operations. The spec allows an empty object too, but I don't consider this important enough to add yet another JSON converter with all its overhead. --- .../Mixed/AtomicSerializationTests.cs | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs index bc9b637617..5ceb8bf4a1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -13,8 +12,6 @@ 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(); @@ -25,11 +22,11 @@ public AtomicSerializationTests(IntegrationTestContext(); // These routes need to be registered in ASP.NET for rendering links to resource/relationship endpoints. - testContext.UseController(); + testContext.UseController(); testContext.ConfigureServicesAfterStartup(services => { - services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>)); + services.AddResourceDefinition(); }); var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); @@ -39,32 +36,46 @@ public AtomicSerializationTests(IntegrationTestContext { - await dbContext.ClearTableAsync(); + dbContext.Performers.Add(existingPerformer); + await dbContext.SaveChangesAsync(); }); var requestBody = new { - atomic__operations = new[] + atomic__operations = new object[] { new { - op = "add", + op = "update", data = new { type = "performers", - id = newPerformer.StringId, + id = existingPerformer.StringId, attributes = new { - artistName = newPerformer.ArtistName, - bornAt = newPerformer.BornAt + } + } + }, + new + { + op = "add", + data = new + { + type = "textLanguages", + id = newLanguage.StringId, + attributes = new + { + isoCode = newLanguage.IsoCode } } } @@ -87,16 +98,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ] }, ""atomic:results"": [ + { + ""data"": null + }, { ""data"": { - ""type"": ""performers"", - ""id"": """ + newPerformer.StringId + @""", + ""type"": ""textLanguages"", + ""id"": """ + newLanguage.StringId + @""", ""attributes"": { - ""artistName"": """ + newPerformer.ArtistName + @""", - ""bornAt"": """ + newPerformer.BornAt.ToString(JsonDateTimeOffsetFormatSpecifier) + @""" + ""isoCode"": """ + newLanguage.IsoCode + @" (changed)"" + }, + ""relationships"": { + ""lyrics"": { + ""links"": { + ""self"": ""http://localhost/textLanguages/" + newLanguage.StringId + @"/relationships/lyrics"", + ""related"": ""http://localhost/textLanguages/" + newLanguage.StringId + @"/lyrics"" + } + } }, ""links"": { - ""self"": ""http://localhost/performers/" + newPerformer.StringId + @""" + ""self"": ""http://localhost/textLanguages/" + newLanguage.StringId + @""" } } } From cab3dc60483c46f10c4d8570dc93cb56beb0e05e Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 5 Oct 2021 11:48:02 +0200 Subject: [PATCH 28/49] Improved tests for includes --- .../Expressions/IncludeElementExpression.cs | 2 +- .../QueryStrings/Includes/IncludeTests.cs | 155 +++++++++++++++++- .../QueryStrings/LoginAttempt.cs | 17 ++ .../QueryStrings/QueryStringDbContext.cs | 1 + .../QueryStrings/QueryStringFakers.cs | 7 + .../QueryStrings/WebAccount.cs | 3 + 6 files changed, 176 insertions(+), 9 deletions(-) create mode 100644 test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/LoginAttempt.cs diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs index a63db4c707..669430ad99 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs @@ -64,7 +64,7 @@ public override bool Equals(object obj) var other = (IncludeElementExpression)obj; - return Relationship.Equals(other.Relationship) == Children.SequenceEqual(other.Children); + return Relationship.Equals(other.Relationship) && Children.SequenceEqual(other.Children); } public override int GetHashCode() diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs index 7236e285eb..ee19cf83f2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs @@ -447,37 +447,176 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); - responseDocument.Data.SingleValue.Attributes["title"].Should().Be(blog.Title); + responseDocument.Data.SingleValue.Relationships["posts"].Data.ManyValue[0].Type.Should().Be("blogPosts"); + responseDocument.Data.SingleValue.Relationships["posts"].Data.ManyValue[0].Id.Should().Be(blog.Posts[0].StringId); responseDocument.Included.Should().HaveCount(7); responseDocument.Included[0].Type.Should().Be("blogPosts"); responseDocument.Included[0].Id.Should().Be(blog.Posts[0].StringId); - responseDocument.Included[0].Attributes["caption"].Should().Be(blog.Posts[0].Caption); + responseDocument.Included[0].Relationships["author"].Data.SingleValue.Type.Should().Be("webAccounts"); + responseDocument.Included[0].Relationships["author"].Data.SingleValue.Id.Should().Be(blog.Posts[0].Author.StringId); + responseDocument.Included[0].Relationships["comments"].Data.ManyValue[0].Type.Should().Be("comments"); + responseDocument.Included[0].Relationships["comments"].Data.ManyValue[0].Id.Should().Be(blog.Posts[0].Comments.ElementAt(0).StringId); responseDocument.Included[1].Type.Should().Be("webAccounts"); responseDocument.Included[1].Id.Should().Be(blog.Posts[0].Author.StringId); - responseDocument.Included[1].Attributes["userName"].Should().Be(blog.Posts[0].Author.UserName); + responseDocument.Included[1].Relationships["preferences"].Data.SingleValue.Type.Should().Be("accountPreferences"); + responseDocument.Included[1].Relationships["preferences"].Data.SingleValue.Id.Should().Be(blog.Posts[0].Author.Preferences.StringId); + responseDocument.Included[1].Relationships["posts"].Data.Value.Should().BeNull(); responseDocument.Included[2].Type.Should().Be("accountPreferences"); responseDocument.Included[2].Id.Should().Be(blog.Posts[0].Author.Preferences.StringId); - responseDocument.Included[2].Attributes["useDarkTheme"].Should().Be(blog.Posts[0].Author.Preferences.UseDarkTheme); responseDocument.Included[3].Type.Should().Be("comments"); responseDocument.Included[3].Id.Should().Be(blog.Posts[0].Comments.ElementAt(0).StringId); - responseDocument.Included[3].Attributes["text"].Should().Be(blog.Posts[0].Comments.ElementAt(0).Text); + responseDocument.Included[3].Relationships["author"].Data.SingleValue.Type.Should().Be("webAccounts"); + responseDocument.Included[3].Relationships["author"].Data.SingleValue.Id.Should().Be(blog.Posts[0].Comments.ElementAt(0).Author.StringId); responseDocument.Included[4].Type.Should().Be("webAccounts"); responseDocument.Included[4].Id.Should().Be(blog.Posts[0].Comments.ElementAt(0).Author.StringId); - responseDocument.Included[4].Attributes["userName"].Should().Be(blog.Posts[0].Comments.ElementAt(0).Author.UserName); + responseDocument.Included[4].Relationships["posts"].Data.ManyValue[0].Type.Should().Be("blogPosts"); + responseDocument.Included[4].Relationships["posts"].Data.ManyValue[0].Id.Should().Be(blog.Posts[0].Comments.ElementAt(0).Author.Posts[0].StringId); + responseDocument.Included[4].Relationships["preferences"].Data.Value.Should().BeNull(); responseDocument.Included[5].Type.Should().Be("blogPosts"); responseDocument.Included[5].Id.Should().Be(blog.Posts[0].Comments.ElementAt(0).Author.Posts[0].StringId); - responseDocument.Included[5].Attributes["caption"].Should().Be(blog.Posts[0].Comments.ElementAt(0).Author.Posts[0].Caption); + responseDocument.Included[5].Relationships["author"].Data.Value.Should().BeNull(); + responseDocument.Included[5].Relationships["comments"].Data.Value.Should().BeNull(); responseDocument.Included[6].Type.Should().Be("comments"); responseDocument.Included[6].Id.Should().Be(blog.Posts[0].Comments.ElementAt(1).StringId); - responseDocument.Included[6].Attributes["text"].Should().Be(blog.Posts[0].Comments.ElementAt(1).Text); + responseDocument.Included[5].Relationships["author"].Data.Value.Should().BeNull(); + } + + [Fact] + public async Task Can_include_chain_of_relationships_with_reused_resources() + { + WebAccount author = _fakers.WebAccount.Generate(); + author.Preferences = _fakers.AccountPreferences.Generate(); + author.LoginAttempts = _fakers.LoginAttempt.Generate(1); + + WebAccount reviewer = _fakers.WebAccount.Generate(); + reviewer.Preferences = _fakers.AccountPreferences.Generate(); + reviewer.LoginAttempts = _fakers.LoginAttempt.Generate(1); + + BlogPost post1 = _fakers.BlogPost.Generate(); + post1.Author = author; + post1.Reviewer = reviewer; + + WebAccount person = _fakers.WebAccount.Generate(); + person.Preferences = _fakers.AccountPreferences.Generate(); + person.LoginAttempts = _fakers.LoginAttempt.Generate(1); + + BlogPost post2 = _fakers.BlogPost.Generate(); + post2.Author = person; + post2.Reviewer = person; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.Posts.AddRange(post1, post2); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/blogPosts?include=reviewer.loginAttempts,author.preferences"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.Should().HaveCount(2); + + responseDocument.Data.ManyValue[0].Type.Should().Be("blogPosts"); + responseDocument.Data.ManyValue[0].Id.Should().Be(post1.StringId); + responseDocument.Data.ManyValue[0].Relationships["author"].Data.SingleValue.Type.Should().Be("webAccounts"); + responseDocument.Data.ManyValue[0].Relationships["author"].Data.SingleValue.Id.Should().Be(author.StringId); + responseDocument.Data.ManyValue[0].Relationships["reviewer"].Data.SingleValue.Type.Should().Be("webAccounts"); + responseDocument.Data.ManyValue[0].Relationships["reviewer"].Data.SingleValue.Id.Should().Be(reviewer.StringId); + + responseDocument.Data.ManyValue[1].Type.Should().Be("blogPosts"); + responseDocument.Data.ManyValue[1].Id.Should().Be(post2.StringId); + responseDocument.Data.ManyValue[1].Relationships["author"].Data.SingleValue.Type.Should().Be("webAccounts"); + responseDocument.Data.ManyValue[1].Relationships["author"].Data.SingleValue.Id.Should().Be(person.StringId); + responseDocument.Data.ManyValue[1].Relationships["reviewer"].Data.SingleValue.Type.Should().Be("webAccounts"); + responseDocument.Data.ManyValue[1].Relationships["reviewer"].Data.SingleValue.Id.Should().Be(person.StringId); + + responseDocument.Included.Should().HaveCount(7); + + responseDocument.Included[0].Type.Should().Be("webAccounts"); + responseDocument.Included[0].Id.Should().Be(author.StringId); + responseDocument.Included[0].Relationships["preferences"].Data.SingleValue.Type.Should().Be("accountPreferences"); + responseDocument.Included[0].Relationships["preferences"].Data.SingleValue.Id.Should().Be(author.Preferences.StringId); + responseDocument.Included[0].Relationships["loginAttempts"].Data.Value.Should().BeNull(); + + responseDocument.Included[1].Type.Should().Be("accountPreferences"); + responseDocument.Included[1].Id.Should().Be(author.Preferences.StringId); + + responseDocument.Included[2].Type.Should().Be("webAccounts"); + responseDocument.Included[2].Id.Should().Be(reviewer.StringId); + responseDocument.Included[2].Relationships["preferences"].Data.Value.Should().BeNull(); + responseDocument.Included[2].Relationships["loginAttempts"].Data.ManyValue[0].Type.Should().Be("loginAttempts"); + responseDocument.Included[2].Relationships["loginAttempts"].Data.ManyValue[0].Id.Should().Be(reviewer.LoginAttempts[0].StringId); + + responseDocument.Included[3].Type.Should().Be("loginAttempts"); + responseDocument.Included[3].Id.Should().Be(reviewer.LoginAttempts[0].StringId); + + responseDocument.Included[4].Type.Should().Be("webAccounts"); + responseDocument.Included[4].Id.Should().Be(person.StringId); + responseDocument.Included[4].Relationships["preferences"].Data.SingleValue.Type.Should().Be("accountPreferences"); + responseDocument.Included[4].Relationships["preferences"].Data.SingleValue.Id.Should().Be(person.Preferences.StringId); + responseDocument.Included[4].Relationships["loginAttempts"].Data.ManyValue[0].Type.Should().Be("loginAttempts"); + responseDocument.Included[4].Relationships["loginAttempts"].Data.ManyValue[0].Id.Should().Be(person.LoginAttempts[0].StringId); + + responseDocument.Included[5].Type.Should().Be("accountPreferences"); + responseDocument.Included[5].Id.Should().Be(person.Preferences.StringId); + + responseDocument.Included[6].Type.Should().Be("loginAttempts"); + responseDocument.Included[6].Id.Should().Be(person.LoginAttempts[0].StringId); + } + + [Fact] + public async Task Can_include_chain_with_cyclic_dependency() + { + List posts = _fakers.BlogPost.Generate(1); + + Blog blog = _fakers.Blog.Generate(); + blog.Posts = posts; + blog.Posts[0].Author = _fakers.WebAccount.Generate(); + blog.Posts[0].Author.Posts = posts; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Blogs.Add(blog); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/blogs/{blog.StringId}?include=posts.author.posts.author.posts.author"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.SingleValue.Type.Should().Be("blogs"); + responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); + responseDocument.Data.SingleValue.Relationships["posts"].Data.ManyValue[0].Type.Should().Be("blogPosts"); + responseDocument.Data.SingleValue.Relationships["posts"].Data.ManyValue[0].Id.Should().Be(blog.Posts[0].StringId); + + responseDocument.Included.Should().HaveCount(2); + + responseDocument.Included[0].Type.Should().Be("blogPosts"); + responseDocument.Included[0].Id.Should().Be(blog.Posts[0].StringId); + responseDocument.Included[0].Relationships["author"].Data.SingleValue.Type.Should().Be("webAccounts"); + responseDocument.Included[0].Relationships["author"].Data.SingleValue.Id.Should().Be(blog.Posts[0].Author.StringId); + + responseDocument.Included[1].Type.Should().Be("webAccounts"); + responseDocument.Included[1].Id.Should().Be(blog.Posts[0].Author.StringId); + responseDocument.Included[1].Relationships["posts"].Data.ManyValue[0].Type.Should().Be("blogPosts"); + responseDocument.Included[1].Relationships["posts"].Data.ManyValue[0].Id.Should().Be(blog.Posts[0].StringId); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/LoginAttempt.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/LoginAttempt.cs new file mode 100644 index 0000000000..d86783f387 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/LoginAttempt.cs @@ -0,0 +1,17 @@ +using System; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreTests.IntegrationTests.QueryStrings +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class LoginAttempt : Identifiable + { + [Attr] + public DateTimeOffset TriedAt { get; set; } + + [Attr] + public bool IsSucceeded { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringDbContext.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringDbContext.cs index b09dfbc278..af11134b1c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringDbContext.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringDbContext.cs @@ -14,6 +14,7 @@ public sealed class QueryStringDbContext : DbContext public DbSet Comments { get; set; } public DbSet Accounts { get; set; } public DbSet AccountPreferences { get; set; } + public DbSet LoginAttempts { get; set; } public DbSet Calendars { get; set; } public DbSet Appointments { get; set; } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringFakers.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringFakers.cs index 702ba9c7f5..54bf83fb01 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringFakers.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringFakers.cs @@ -43,6 +43,12 @@ internal sealed class QueryStringFakers : FakerContainer .RuleFor(webAccount => webAccount.DateOfBirth, faker => faker.Person.DateOfBirth) .RuleFor(webAccount => webAccount.EmailAddress, faker => faker.Internet.Email())); + private readonly Lazy> _lazyLoginAttemptFaker = new(() => + new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(loginAttempt => loginAttempt.TriedAt, faker => faker.Date.PastOffset()) + .RuleFor(loginAttempt => loginAttempt.IsSucceeded, faker => faker.Random.Bool())); + private readonly Lazy> _lazyAccountPreferencesFaker = new(() => new Faker() .UseSeed(GetFakerSeed()) @@ -67,6 +73,7 @@ internal sealed class QueryStringFakers : FakerContainer public Faker + internal sealed class RevertRequestStateOnDispose : IDisposable + { + private readonly IJsonApiRequest _sourceRequest; + private readonly ITargetedFields _sourceTargetedFields; + + private readonly IJsonApiRequest _backupRequest = new JsonApiRequest(); + private readonly ITargetedFields _backupTargetedFields = new TargetedFields(); + + public RevertRequestStateOnDispose(IJsonApiRequest request, ITargetedFields targetedFields) + { + ArgumentGuard.NotNull(request, nameof(request)); + + _sourceRequest = request; + _backupRequest.CopyFrom(request); + + if (targetedFields != null) + { + _sourceTargetedFields = targetedFields; + _backupTargetedFields.CopyFrom(targetedFields); + } + } + + public void Dispose() + { + _sourceRequest.CopyFrom(_backupRequest); + _sourceTargetedFields?.CopyFrom(_backupTargetedFields); + } + } +} diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 9f3c6cc04e..171bc90d2e 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -259,7 +259,6 @@ private void AddSerializationLayer() _services.AddScoped(); _services.AddScoped(typeof(ResponseSerializer<>)); _services.AddScoped(typeof(AtomicOperationsResponseSerializer)); - _services.AddScoped(sp => sp.GetRequiredService().GetSerializer()); _services.AddScoped(); _services.AddSingleton(); _services.AddSingleton(); @@ -274,6 +273,9 @@ private void AddSerializationLayer() _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); + + _services.AddScoped(); + _services.AddScoped(); } private void AddOperationsLayer() diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs index 5354522a55..e4bb444dc1 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs @@ -130,6 +130,7 @@ protected virtual void ValidateModelState(IEnumerable operat var violations = new List(); int index = 0; + using IDisposable _ = new RevertRequestStateOnDispose(_request, _targetedFields); foreach (OperationContainer operation in operations) { diff --git a/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs b/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs index 9160791f87..47e64d8338 100644 --- a/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs +++ b/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Reflection; #pragma warning disable AV1008 // Class should not be static @@ -32,7 +33,7 @@ public static ICodeTimer Current static CodeTimingSessionManager() { #if DEBUG - IsEnabled = !IsRunningInTest(); + IsEnabled = !IsRunningInTest() && !IsRunningInBenchmark(); #else IsEnabled = false; #endif @@ -47,6 +48,12 @@ private static bool IsRunningInTest() assembly.FullName != null && assembly.FullName.StartsWith(testAssemblyName, StringComparison.Ordinal)); } + // ReSharper disable once UnusedMember.Local + private static bool IsRunningInBenchmark() + { + return Assembly.GetEntryAssembly()?.GetName().Name == "Benchmarks"; + } + private static void AssertHasActiveSession() { if (_session == null) diff --git a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs index e4f11541ab..fd341166ab 100644 --- a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs @@ -90,7 +90,8 @@ private string SerializeOperationsDocument(IEnumerable opera var document = new Document { Results = operations.Select(SerializeOperation).ToList(), - Meta = _metaBuilder.Build() + Meta = _metaBuilder.Build(), + Links = _linkBuilder.GetTopLevelLinks() }; SetApiVersion(document); @@ -132,7 +133,7 @@ private AtomicResultObject SerializeOperation(OperationContainer operation) resourceObject = ResourceObjectBuilder.Build(operation.Resource, attributes, relationships); } - if (resourceObject != null) + if (resourceObject != null && operation.Request.Kind != EndpointKind.Relationship) { resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); } @@ -145,6 +146,7 @@ private AtomicResultObject SerializeOperation(OperationContainer operation) private string SerializeErrorDocument(Document document) { + document.Links = _linkBuilder.GetTopLevelLinks(); SetApiVersion(document); return SerializeObject(document, _options.SerializerWriteOptions); diff --git a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs index d096a4ea6c..b6bfc14b85 100644 --- a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs @@ -43,8 +43,6 @@ protected BaseSerializer(IResourceObjectBuilder resourceObjectBuilder) protected Document Build(IIdentifiable resource, IReadOnlyCollection attributes, IReadOnlyCollection relationships) { - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Serializer.Build (single)"); - ResourceObject resourceObject = resource != null ? ResourceObjectBuilder.Build(resource, attributes, relationships) : null; return new Document @@ -74,8 +72,6 @@ protected Document Build(IReadOnlyCollection resources, IReadOnly { ArgumentGuard.NotNull(resources, nameof(resources)); - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Serializer.Build (list)"); - var resourceObjects = new List(); foreach (IIdentifiable resource in resources) diff --git a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs index ad82acff5b..9dde02d308 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs @@ -72,7 +72,7 @@ public TopLevelLinks GetTopLevelLinks() links.Self = GetLinkForTopLevelSelf(); } - if (_request.Kind == EndpointKind.Relationship && ShouldIncludeTopLevelLink(LinkTypes.Related, requestContext)) + if (_request.Kind == EndpointKind.Relationship && _request.Relationship != null && ShouldIncludeTopLevelLink(LinkTypes.Related, requestContext)) { links.Related = GetLinkForRelationshipRelated(_request.PrimaryId, _request.Relationship); } @@ -91,7 +91,7 @@ public TopLevelLinks GetTopLevelLinks() /// private bool ShouldIncludeTopLevelLink(LinkTypes linkType, ResourceContext resourceContext) { - if (resourceContext.TopLevelLinks != LinkTypes.NotConfigured) + if (resourceContext != null && resourceContext.TopLevelLinks != LinkTypes.NotConfigured) { return resourceContext.TopLevelLinks.HasFlag(linkType); } @@ -232,7 +232,7 @@ public ResourceLinks GetResourceLinks(string resourceName, string id) var links = new ResourceLinks(); ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceName); - if (_request.Kind != EndpointKind.Relationship && ShouldIncludeResourceLink(LinkTypes.Self, resourceContext)) + if (ShouldIncludeResourceLink(LinkTypes.Self, resourceContext)) { links.Self = GetLinkForResourceSelf(resourceContext, id); } diff --git a/src/JsonApiDotNetCore/Serialization/IResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/IResponseModelAdapter.cs new file mode 100644 index 0000000000..447725c10b --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/IResponseModelAdapter.cs @@ -0,0 +1,47 @@ +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization +{ + /// + /// Converts the produced model from an ASP.NET controller action into a , ready to be serialized as the response body. + /// + public interface IResponseModelAdapter + { + /// + /// Validates and converts the specified . Supported model types: + /// + /// + /// + /// ]]> + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// + /// + /// + /// + /// ]]> + /// + /// + /// + /// + /// + /// + /// + /// + /// + (Document responseDocument, string contentType) Convert(object model); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs index d31739ec9c..52244ba2c3 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs @@ -44,51 +44,54 @@ public async Task WriteAsync(object model, HttpContext httpContext) { ArgumentGuard.NotNull(httpContext, nameof(httpContext)); - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Write response body"); - HttpRequest request = httpContext.Request; HttpResponse response = httpContext.Response; await using TextWriter writer = new HttpResponseStreamWriter(response.Body, Encoding.UTF8); string responseContent; - try + using (IDisposable _ = CodeTimingSessionManager.Current.Measure("Write response body")) { - responseContent = SerializeResponse(model, (HttpStatusCode)response.StatusCode); - } + try + { + responseContent = SerializeResponse(model, (HttpStatusCode)response.StatusCode); + } #pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException - catch (Exception exception) + catch (Exception exception) #pragma warning restore AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException - { - IReadOnlyList errors = _exceptionHandler.HandleException(exception); - responseContent = _serializer.Serialize(errors); - response.StatusCode = (int)ErrorObject.GetResponseStatusCode(errors); - } + { + IReadOnlyList errors = _exceptionHandler.HandleException(exception); + responseContent = _serializer.Serialize(errors); + response.StatusCode = (int)ErrorObject.GetResponseStatusCode(errors); + } - bool hasMatchingETag = SetETagResponseHeader(request, response, responseContent); + bool hasMatchingETag = SetETagResponseHeader(request, response, responseContent); - if (hasMatchingETag) - { - response.StatusCode = (int)HttpStatusCode.NotModified; - responseContent = string.Empty; - } + if (hasMatchingETag) + { + response.StatusCode = (int)HttpStatusCode.NotModified; + responseContent = string.Empty; + } - if (request.Method == HttpMethod.Head.Method) - { - responseContent = string.Empty; + if (request.Method == HttpMethod.Head.Method) + { + responseContent = string.Empty; + } + + if (!string.IsNullOrEmpty(responseContent)) + { + response.ContentType = _serializer.ContentType; + } } - string url = request.GetEncodedUrl(); + _traceWriter.LogMessage(() => + $"Sending {response.StatusCode} response for {request.Method} request at '{request.GetEncodedUrl()}' with body: <<{responseContent}>>"); - if (!string.IsNullOrEmpty(responseContent)) + using (IDisposable _ = CodeTimingSessionManager.Current.Measure("Send response body")) { - response.ContentType = _serializer.ContentType; + await writer.WriteAsync(responseContent); + await writer.FlushAsync(); } - - _traceWriter.LogMessage(() => $"Sending {response.StatusCode} response for {request.Method} request at '{url}' with body: <<{responseContent}>>"); - - await writer.WriteAsync(responseContent); - await writer.FlushAsync(); } private string SerializeResponse(object contextObject, HttpStatusCode statusCode) diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterState.cs b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterState.cs index 1d9ea10f08..80ab398dd7 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterState.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterState.cs @@ -1,5 +1,6 @@ using System; using JetBrains.Annotations; +using JsonApiDotNetCore.AtomicOperations; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; @@ -11,8 +12,7 @@ namespace JsonApiDotNetCore.Serialization.RequestAdapters [PublicAPI] public sealed class RequestAdapterState : IDisposable { - private static readonly TargetedFields EmptyTargetedFields = new(); - private readonly IJsonApiRequest _backupRequest; + private readonly IDisposable _backupRequestState; public IJsonApiRequest InjectableRequest { get; } public ITargetedFields InjectableTargetedFields { get; } @@ -33,8 +33,7 @@ public RequestAdapterState(IJsonApiRequest request, ITargetedFields targetedFiel if (request.Kind == EndpointKind.AtomicOperations) { - _backupRequest = new JsonApiRequest(); - _backupRequest.CopyFrom(InjectableRequest); + _backupRequestState = new RevertRequestStateOnDispose(request, targetedFields); } } @@ -56,10 +55,9 @@ public void Dispose() // For resource requests, we'd like the injected state to become the final state. // But for operations, it makes more sense to reset than to reflect the last operation. - if (_backupRequest != null) + if (_backupRequestState != null) { - InjectableTargetedFields.CopyFrom(EmptyTargetedFields); - InjectableRequest.CopyFrom(_backupRequest); + _backupRequestState.Dispose(); } else { diff --git a/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs new file mode 100644 index 0000000000..2731c8d4a4 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs @@ -0,0 +1,379 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.Json.Serialization; +using JsonApiDotNetCore.AtomicOperations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Queries.Internal; +using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Resources.Internal; +using JsonApiDotNetCore.Serialization.Building; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization +{ + /// + public sealed class ResponseModelAdapter : IResponseModelAdapter + { + private static readonly CollectionConverter CollectionConverter = new(); + + private readonly IJsonApiRequest _request; + private readonly IJsonApiOptions _options; + private readonly IResourceGraph _resourceGraph; + private readonly ILinkBuilder _linkBuilder; + private readonly IMetaBuilder _metaBuilder; + private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; + private readonly IEvaluatedIncludeCache _evaluatedIncludeCache; + private readonly IRequestQueryStringAccessor _requestQueryStringAccessor; + private readonly ISparseFieldSetCache _sparseFieldSetCache; + + public ResponseModelAdapter(IJsonApiRequest request, IJsonApiOptions options, IResourceGraph resourceGraph, ILinkBuilder linkBuilder, + IMetaBuilder metaBuilder, IResourceDefinitionAccessor resourceDefinitionAccessor, IEvaluatedIncludeCache evaluatedIncludeCache, + ISparseFieldSetCache sparseFieldSetCache, IRequestQueryStringAccessor requestQueryStringAccessor) + { + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); + ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder)); + ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); + ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache)); + ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); + ArgumentGuard.NotNull(requestQueryStringAccessor, nameof(requestQueryStringAccessor)); + + _request = request; + _options = options; + _resourceGraph = resourceGraph; + _linkBuilder = linkBuilder; + _metaBuilder = metaBuilder; + _resourceDefinitionAccessor = resourceDefinitionAccessor; + _evaluatedIncludeCache = evaluatedIncludeCache; + _sparseFieldSetCache = sparseFieldSetCache; + _requestQueryStringAccessor = requestQueryStringAccessor; + } + + /// + public (Document responseDocument, string contentType) Convert(object model) + { + _sparseFieldSetCache.Reset(); + + var document = new Document(); + + IncludeExpression include = _evaluatedIncludeCache.Get(); + IImmutableList includeElements = include?.Elements ?? ImmutableArray.Empty; + + var includedCollection = new IncludedCollection(); + + if (model is IEnumerable resources) + { + IEnumerable resourceObjects = + resources.Select(resource => ConvertResource(resource, _request.Kind, includeElements, includedCollection, false)); + + document.Data = new SingleOrManyData(resourceObjects); + } + else if (model is IIdentifiable resource) + { + ResourceObject resourceObject = ConvertResource(resource, _request.Kind, includeElements, includedCollection, false); + document.Data = new SingleOrManyData(resourceObject); + } + else if (model == null) + { + document.Data = new SingleOrManyData(null); + } + else if (model is IEnumerable operations) + { + using var _ = new RevertRequestStateOnDispose(_request, null); + document.Results = operations.Select(operation => ConvertOperation(operation, includeElements, includedCollection)).ToList(); + } + else if (model is IEnumerable errorObjects) + { + document.Errors = errorObjects.ToArray(); + } + else if (model is ErrorObject errorObject) + { + document.Errors = errorObject.AsArray(); + } + else + { + throw new InvalidOperationException("Data being returned must be resources, operations, errors or null."); + } + + document.JsonApi = GetApiObject(); + document.Links = _linkBuilder.GetTopLevelLinks(); + document.Meta = _metaBuilder.Build(); + document.Included = GetIncluded(includedCollection); + + string contentType = _request.Kind == EndpointKind.AtomicOperations ? HeaderConstants.AtomicOperationsMediaType : HeaderConstants.MediaType; + return (document, contentType); + } + + private AtomicResultObject ConvertOperation(OperationContainer operation, IImmutableList includeElements, + IncludedCollection includedCollection) + { + ResourceObject resourceObject = null; + + if (operation != null) + { + _request.CopyFrom(operation.Request); + + resourceObject = ConvertResource(operation.Resource, operation.Request.Kind, includeElements, includedCollection, false); + + _sparseFieldSetCache.Reset(); + } + + return new AtomicResultObject + { + Data = resourceObject == null ? default : new SingleOrManyData(resourceObject) + }; + } + + private ResourceObject ConvertResource(IIdentifiable resource, EndpointKind requestKind, IImmutableList includeElements, + IncludedCollection includedCollection, bool isInclude) + { + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resource.GetType()); + IImmutableSet fieldSet = null; + + if (requestKind != EndpointKind.Relationship) + { + _resourceDefinitionAccessor.OnSerialize(resource); + + fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); + } + + var resourceObject = new ResourceObject(); + + if (isInclude) + { + resourceObject = includedCollection.AddOrUpdate(resource, resourceObject); + } + + bool isRelationship = requestKind == EndpointKind.Relationship; + + resourceObject.Type = resourceContext.PublicName; + resourceObject.Id = resource.StringId; + resourceObject.Attributes = ConvertAttributes(resource, resourceContext, fieldSet); + resourceObject.Relationships = ConvertRelationships(resource, resourceContext, fieldSet, requestKind, includeElements, includedCollection); + resourceObject.Links = isRelationship ? null : _linkBuilder.GetResourceLinks(resourceContext.PublicName, resource.StringId); + resourceObject.Meta = isRelationship ? null : _resourceDefinitionAccessor.GetMeta(resource.GetType(), resource); + + return resourceObject; + } + + private IDictionary ConvertAttributes(IIdentifiable resource, ResourceContext resourceContext, + IImmutableSet fieldSet) + { + if (fieldSet != null) + { + var attrMap = new Dictionary(resourceContext.Attributes.Count); + + foreach (AttrAttribute attr in resourceContext.Attributes) + { + if (!fieldSet.Contains(attr) || attr.Property.Name == nameof(Identifiable.Id)) + { + continue; + } + + object value = attr.GetValue(resource); + + if (_options.SerializerOptions.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull && value == null) + { + continue; + } + + if (_options.SerializerOptions.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingDefault && + Equals(value, RuntimeTypeConverter.GetDefaultValue(attr.Property.PropertyType))) + { + continue; + } + + attrMap.Add(attr.PublicName, value); + } + + if (attrMap.Any()) + { + return attrMap; + } + } + + return null; + } + + private IDictionary ConvertRelationships(IIdentifiable resource, ResourceContext resourceContext, + IImmutableSet fieldSet, EndpointKind requestKind, IImmutableList includeElements, + IncludedCollection includedCollection) + { + if (fieldSet != null) + { + var relationshipMap = new Dictionary(resourceContext.Relationships.Count); + + foreach (RelationshipAttribute relationship in resourceContext.Relationships) + { + IncludeElementExpression includeElement = includeElements.FirstOrDefault(element => element.Relationship.Equals(relationship)); + RelationshipObject relationshipObject = ConvertRelationship(relationship, resource, requestKind, includeElement, includedCollection); + + if (relationshipObject != null && fieldSet.Contains(relationship)) + { + relationshipMap.Add(relationship.PublicName, relationshipObject); + } + } + + if (relationshipMap.Any()) + { + return relationshipMap; + } + } + + return null; + } + + private RelationshipObject ConvertRelationship(RelationshipAttribute relationship, IIdentifiable leftResource, EndpointKind requestKind, + IncludeElementExpression includeElement, IncludedCollection includedCollection) + { + SingleOrManyData data = default; + + if (includeElement != null) + { + object rightValue = relationship.GetValue(leftResource); + ICollection rightResources = CollectionConverter.ExtractResources(rightValue); + + var resourceIdentifierObjects = new List(rightResources.Count); + + foreach (IIdentifiable rightResource in rightResources) + { + var resourceIdentifierObject = new ResourceIdentifierObject + { + Type = _resourceGraph.GetResourceContext(rightResource.GetType()).PublicName, + Id = rightResource.StringId + }; + + resourceIdentifierObjects.Add(resourceIdentifierObject); + + ResourceObject includeResource = ConvertResource(rightResource, requestKind, includeElement.Children, includedCollection, true); + includedCollection.AddOrUpdate(rightResource, includeResource); + } + + data = relationship is HasOneAttribute + ? new SingleOrManyData(resourceIdentifierObjects.SingleOrDefault()) + : new SingleOrManyData(resourceIdentifierObjects); + } + + RelationshipLinks links = _linkBuilder.GetRelationshipLinks(relationship, leftResource); + + return links == null && !data.IsAssigned + ? null + : new RelationshipObject + { + Links = links, + Data = data + }; + } + + private JsonApiObject GetApiObject() + { + if (!_options.IncludeJsonApiVersion) + { + return null; + } + + var jsonApiObject = new JsonApiObject + { + Version = "1.1" + }; + + if (_request.Kind == EndpointKind.AtomicOperations) + { + jsonApiObject.Ext = new List + { + "https://jsonapi.org/ext/atomic" + }; + } + + return jsonApiObject; + } + + private IList GetIncluded(IncludedCollection includedCollection) + { + if (includedCollection.ResourceObjects.Any()) + { + return includedCollection.ResourceObjects; + } + + return _requestQueryStringAccessor.Query.ContainsKey("include") ? Array.Empty() : null; + } + + private sealed class IncludedCollection + { + private readonly List _includes = new(); + private readonly Dictionary _resourceToIncludeIndexMap = new(IdentifiableComparer.Instance); + + public IList ResourceObjects => _includes; + + public ResourceObject AddOrUpdate(IIdentifiable resource, ResourceObject resourceObject) + { + if (!_resourceToIncludeIndexMap.ContainsKey(resource)) + { + _includes.Add(resourceObject); + _resourceToIncludeIndexMap.Add(resource, _includes.Count - 1); + } + else + { + if (resourceObject.Type != null) + { + int existingIndex = _resourceToIncludeIndexMap[resource]; + ResourceObject existingVersion = _includes[existingIndex]; + + if (existingVersion != resourceObject) + { + MergeRelationships(resourceObject, existingVersion); + + return existingVersion; + } + } + } + + return resourceObject; + } + + private static void MergeRelationships(ResourceObject incomingVersion, ResourceObject existingVersion) + { + // The code below handles the case where one resource is added through different include chains with different relationships. + // We enrich the existing resource object with the added relationships coming from the second chain, to ensure correct resource linkage. + // + // This is best explained using an example. Consider the next inclusion chains: + // + // 1. reviewer.loginAttempts + // 2. author.preferences + // + // Where the relationships `reviewer` and `author` are of the same resource type `people`. Then the next rules apply: + // + // A. People that were included as reviewers from inclusion chain (1) should come with their `loginAttempts` included, but not those from chain (2). + // B. People that were included as authors from inclusion chain (2) should come with their `preferences` included, but not those from chain (1). + // C. For a person that was included as both an reviewer and author (i.e. targeted by both chains), both `loginAttempts` and `preferences` need + // to be present. + // + // For rule (C), the related resources will be included as usual, but we need to fix resource linkage here by merging the relationship objects. + // + // Note that this implementation breaks the overall depth-first ordering of included objects. So solve that, we'd need to use a dependency graph + // for included objects instead of a flat list, which may affect performance. Since the ordering is not guaranteed anyway, keeping it simple for now. + + foreach ((string relationshipName, RelationshipObject relationshipObject) in existingVersion.Relationships.EmptyIfNull()) + { + if (!relationshipObject.Data.IsAssigned) + { + SingleOrManyData incomingRelationshipData = incomingVersion.Relationships[relationshipName].Data; + + if (incomingRelationshipData.IsAssigned) + { + relationshipObject.Data = incomingRelationshipData; + } + } + } + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs index 02ef21f177..f795e429fd 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs @@ -34,6 +34,7 @@ public class ResponseSerializer : BaseSerializer, IJsonApiSerializer private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly ISparseFieldSetCache _sparseFieldSetCache; private readonly IJsonApiOptions _options; + private readonly IJsonApiRequest _request; private readonly Type _primaryResourceType; /// @@ -41,7 +42,7 @@ public class ResponseSerializer : BaseSerializer, IJsonApiSerializer public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, IFieldsToSerialize fieldsToSerialize, IResourceObjectBuilder resourceObjectBuilder, IResourceDefinitionAccessor resourceDefinitionAccessor, - ISparseFieldSetCache sparseFieldSetCache, IJsonApiOptions options) + ISparseFieldSetCache sparseFieldSetCache, IJsonApiOptions options, IJsonApiRequest request) : base(resourceObjectBuilder) { ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder)); @@ -51,6 +52,7 @@ public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, II ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(request, nameof(request)); _metaBuilder = metaBuilder; _linkBuilder = linkBuilder; @@ -59,6 +61,7 @@ public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, II _resourceDefinitionAccessor = resourceDefinitionAccessor; _sparseFieldSetCache = sparseFieldSetCache; _options = options; + _request = request; _primaryResourceType = typeof(TResource); } @@ -97,11 +100,12 @@ public string Serialize(object content) return SerializeErrorDocument(errorDocument); } - throw new InvalidOperationException("Data being returned must be null, errors or resources."); + throw new InvalidOperationException("Data being returned must be resources, operations, errors or null."); } private string SerializeErrorDocument(Document document) { + document.Links = _linkBuilder.GetTopLevelLinks(); SetApiVersion(document); return SerializeObject(document, _options.SerializerWriteOptions); @@ -126,7 +130,7 @@ internal string SerializeSingle(IIdentifiable resource) Document document = Build(resource, attributes, relationships); ResourceObject resourceObject = document.Data.SingleValue; - if (resourceObject != null) + if (resourceObject != null && _request.Kind != EndpointKind.Relationship) { resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); } @@ -159,14 +163,9 @@ internal string SerializeMany(IReadOnlyCollection resources) foreach (ResourceObject resourceObject in document.Data.ManyValue) { - ResourceLinks links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); - - if (links == null) - { - break; - } - - resourceObject.Links = links; + resourceObject.Links = _request.Kind != EndpointKind.Relationship + ? _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id) + : null; } AddTopLevelObjects(document); diff --git a/src/JsonApiDotNetCore/Serialization/TemporarySerializerBridge.cs b/src/JsonApiDotNetCore/Serialization/TemporarySerializerBridge.cs new file mode 100644 index 0000000000..af649283b5 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/TemporarySerializerBridge.cs @@ -0,0 +1,61 @@ +using System; +using System.Text.Json; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Diagnostics; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization +{ + /// + /// Acts as a bridge between the legacy and new response serialization implementation. To be removed in a future commit, but handy for the moment to + /// quickly toggle and see what breaks. + /// + public sealed class TemporarySerializerBridge : IJsonApiSerializer + { + private readonly IResponseModelAdapter _responseModelAdapter; + private readonly IJsonApiSerializerFactory _factory; + private readonly IJsonApiOptions _options; + + public string ContentType { get; private set; } + + public TemporarySerializerBridge(IResponseModelAdapter responseModelAdapter, IJsonApiSerializerFactory factory, IJsonApiOptions options) + { + ArgumentGuard.NotNull(responseModelAdapter, nameof(responseModelAdapter)); + ArgumentGuard.NotNull(factory, nameof(factory)); + ArgumentGuard.NotNull(options, nameof(options)); + + _responseModelAdapter = responseModelAdapter; + _factory = factory; + _options = options; + } + + public string Serialize(object model) + { + if (UseLegacySerializer(model)) + { + IJsonApiSerializer serializer = _factory.GetSerializer(); + string responseBody = serializer.Serialize(model); + ContentType = serializer.ContentType; + return responseBody; + } + + (Document document, string contentType) = _responseModelAdapter.Convert(model); + ContentType = contentType; + + return SerializeObject(document, _options.SerializerWriteOptions); + } + + private bool UseLegacySerializer(object model) + { + return false; + } + + private string SerializeObject(object value, JsonSerializerOptions serializerOptions) + { + using IDisposable _ = + CodeTimingSessionManager.Current.Measure("JsonSerializer.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); + + return JsonSerializer.Serialize(value, serializerOptions); + } + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs index 5ceb8bf4a1..f034fb9411 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs @@ -97,6 +97,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""https://jsonapi.org/ext/atomic"" ] }, + ""links"": { + ""self"": ""http://localhost/operations"" + }, ""atomic:results"": [ { ""data"": null @@ -164,6 +167,9 @@ public async Task Includes_version_with_ext_on_error_in_operations_endpoint() ""https://jsonapi.org/ext/atomic"" ] }, + ""links"": { + ""self"": ""http://localhost/operations"" + }, ""errors"": [ { ""id"": """ + errorId + @""", diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs index 5a02e47292..5168931a82 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs @@ -100,7 +100,7 @@ public async Task Cannot_convert_ActionResult_with_string_parameter_to_error_col 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 null, errors or resources."); + error.Detail.Should().Be("Data being returned must be resources, operations, errors or null."); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs index f73e03004d..675c9f25ad 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs @@ -290,6 +290,9 @@ public async Task Cannot_get_unknown_primary_resource_by_ID() string errorId = JsonApiStringConverter.ExtractErrorId(responseDocument); responseDocument.Should().BeJson(@"{ + ""links"": { + ""self"": ""http://localhost/meetings/ffffffff-ffff-ffff-ffff-ffffffffffff"" + }, ""errors"": [ { ""id"": """ + errorId + @""", @@ -761,6 +764,9 @@ public async Task Includes_version_on_error_in_resource_endpoint() ""jsonapi"": { ""version"": ""1.1"" }, + ""links"": { + ""self"": ""http://localhost/meetingAttendees/ffffffff-ffff-ffff-ffff-ffffffffffff"" + }, ""errors"": [ { ""id"": """ + errorId + @""", diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index ba82d9382a..11c2418ffa 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -4,6 +4,7 @@ using System.Linq; using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Queries.Internal; @@ -68,8 +69,10 @@ protected ResponseSerializer GetResponseSerializer(IEnumerable(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, resourceDefinitionAccessor, - sparseFieldSetCache, options); + sparseFieldSetCache, options, request); } protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(IEnumerable> inclusionChains = null, From 055b93f23640cf2e3bf29694e54fca5af3774606 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Oct 2021 11:49:10 +0200 Subject: [PATCH 30/49] Avoid closure in hot code path to reduce allocations --- .../Serialization/ResponseModelAdapter.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs index 2731c8d4a4..b6ca115495 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs @@ -213,7 +213,9 @@ private IDictionary ConvertRelationships(IIdentifiab foreach (RelationshipAttribute relationship in resourceContext.Relationships) { - IncludeElementExpression includeElement = includeElements.FirstOrDefault(element => element.Relationship.Equals(relationship)); + IncludeElementExpression includeElement = GetFirstOrDefault(includeElements, relationship, + (element, nextRelationship) => element.Relationship.Equals(nextRelationship)); + RelationshipObject relationshipObject = ConvertRelationship(relationship, resource, requestKind, includeElement, includedCollection); if (relationshipObject != null && fieldSet.Contains(relationship)) @@ -231,6 +233,22 @@ private IDictionary ConvertRelationships(IIdentifiab return null; } + private static TSource GetFirstOrDefault(IEnumerable source, TContext context, Func condition) + { + // PERF: This replacement for Enumerable.FirstOrDefault() doesn't allocate a compiler-generated closure class <>c__DisplayClass. + // https://www.jetbrains.com/help/resharper/2021.2/Fixing_Issues_Found_by_DPA.html#closures-in-lambda-expressions + + foreach (TSource item in source) + { + if (condition(item, context)) + { + return item; + } + } + + return default; + } + private RelationshipObject ConvertRelationship(RelationshipAttribute relationship, IIdentifiable leftResource, EndpointKind requestKind, IncludeElementExpression includeElement, IncludedCollection includedCollection) { From a7c6009b9530d4212ca4f15fa11e662bb801f00f Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Oct 2021 14:01:03 +0200 Subject: [PATCH 31/49] Cleanup reader and writer --- .../OperationsSerializationBenchmarks.cs | 2 +- .../ResourceSerializationBenchmarks.cs | 2 +- .../AtomicOperationsResponseSerializer.cs | 3 - .../Serialization/IJsonApiSerializer.cs | 5 - .../Serialization/IResponseModelAdapter.cs | 2 +- .../Serialization/JsonApiReader.cs | 24 +-- .../Serialization/JsonApiWriter.cs | 137 +++++++++++------- .../Serialization/ResponseModelAdapter.cs | 5 +- .../Serialization/ResponseSerializer.cs | 3 - .../TemporarySerializerBridge.cs | 10 +- 10 files changed, 101 insertions(+), 92 deletions(-) diff --git a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs index 45748d5235..ac10ed3cfe 100644 --- a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs +++ b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs @@ -116,7 +116,7 @@ private static IEnumerable CreateResponseOperations(IJsonApi [Benchmark] public string SerializeOperationsResponse() { - (Document responseDocument, _) = ResponseModelAdapter.Convert(_responseOperations); + Document responseDocument = ResponseModelAdapter.Convert(_responseOperations); return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions); } diff --git a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs index a2c27cfde0..0271e2abc1 100644 --- a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs +++ b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs @@ -107,7 +107,7 @@ private static ResourceA CreateResponseResource() [Benchmark] public string SerializeResourceResponse() { - (Document responseDocument, _) = ResponseModelAdapter.Convert(ResponseResource); + Document responseDocument = ResponseModelAdapter.Convert(ResponseResource); return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions); } diff --git a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs index fd341166ab..84f388b46a 100644 --- a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs @@ -27,9 +27,6 @@ public sealed class AtomicOperationsResponseSerializer : BaseSerializer, IJsonAp private readonly IJsonApiRequest _request; private readonly IJsonApiOptions _options; - /// - public string ContentType { get; } = HeaderConstants.AtomicOperationsMediaType; - public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectBuilder, IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, IFieldsToSerialize fieldsToSerialize, IResourceDefinitionAccessor resourceDefinitionAccessor, IEvaluatedIncludeCache evaluatedIncludeCache, ISparseFieldSetCache sparseFieldSetCache, IJsonApiRequest request, IJsonApiOptions options) diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs index 97f0a15747..29d09a2c36 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs @@ -5,11 +5,6 @@ namespace JsonApiDotNetCore.Serialization /// public interface IJsonApiSerializer { - /// - /// Gets the Content-Type HTTP header value. - /// - string ContentType { get; } - /// /// Serializes a single resource or a collection of resources. /// diff --git a/src/JsonApiDotNetCore/Serialization/IResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/IResponseModelAdapter.cs index 447725c10b..7fed72c401 100644 --- a/src/JsonApiDotNetCore/Serialization/IResponseModelAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/IResponseModelAdapter.cs @@ -42,6 +42,6 @@ public interface IResponseModelAdapter /// /// /// - (Document responseDocument, string contentType) Convert(object model); + Document Convert(object model); } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index 7fb8ddd7fd..7d1de70925 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -1,6 +1,6 @@ using System; -using System.IO; using System.Net; +using System.Text; using System.Text.Json; using System.Threading.Tasks; using JetBrains.Annotations; @@ -12,6 +12,7 @@ using JsonApiDotNetCore.Serialization.RequestAdapters; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Serialization @@ -39,18 +40,19 @@ public async Task ReadAsync(HttpRequest httpRequest) { ArgumentGuard.NotNull(httpRequest, nameof(httpRequest)); - string requestBody = await GetRequestBodyAsync(httpRequest); + string requestBody = await ReceiveRequestBodyAsync(httpRequest); + + _traceWriter.LogMessage(() => $"Received {httpRequest.Method} request at '{httpRequest.GetEncodedUrl()}' with body: <<{requestBody}>>"); + return GetModel(requestBody); } - private async Task GetRequestBodyAsync(HttpRequest httpRequest) + private static async Task ReceiveRequestBodyAsync(HttpRequest httpRequest) { - using var reader = new StreamReader(httpRequest.Body, leaveOpen: true); - string requestBody = await reader.ReadToEndAsync(); - - _traceWriter.LogMessage(() => $"Received {httpRequest.Method} request at '{httpRequest.GetEncodedUrl()}' with body: <<{requestBody}>>"); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Receive request body"); - return requestBody; + using var reader = new HttpRequestStreamReader(httpRequest.Body, Encoding.UTF8); + return await reader.ReadToEndAsync(); } private object GetModel(string requestBody) @@ -59,7 +61,7 @@ private object GetModel(string requestBody) using IDisposable _ = CodeTimingSessionManager.Current.Measure("Read request body"); - Document document = DeserializeDocument(requestBody, _options.SerializerReadOptions); + Document document = DeserializeDocument(requestBody); return ConvertDocumentToModel(document, requestBody); } @@ -72,14 +74,14 @@ private static void AssertHasRequestBody(string requestBody) } } - private Document DeserializeDocument(string requestBody, JsonSerializerOptions serializerOptions) + private Document DeserializeDocument(string requestBody) { try { using IDisposable _ = CodeTimingSessionManager.Current.Measure("JsonSerializer.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - return JsonSerializer.Deserialize(requestBody, serializerOptions); + return JsonSerializer.Deserialize(requestBody, _options.SerializerReadOptions); } catch (JsonException exception) { diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs index 52244ba2c3..0b73e2f654 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs @@ -4,7 +4,9 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; @@ -21,19 +23,26 @@ namespace JsonApiDotNetCore.Serialization /// public sealed class JsonApiWriter : IJsonApiWriter { - private readonly IJsonApiSerializer _serializer; + private readonly IJsonApiRequest _request; + private readonly IJsonApiOptions _options; + private readonly IResponseModelAdapter _responseModelAdapter; private readonly IExceptionHandler _exceptionHandler; private readonly IETagGenerator _eTagGenerator; private readonly TraceLogWriter _traceWriter; - public JsonApiWriter(IJsonApiSerializer serializer, IExceptionHandler exceptionHandler, IETagGenerator eTagGenerator, ILoggerFactory loggerFactory) + public JsonApiWriter(IJsonApiRequest request, IJsonApiOptions options, IResponseModelAdapter responseModelAdapter, IExceptionHandler exceptionHandler, + IETagGenerator eTagGenerator, ILoggerFactory loggerFactory) { - ArgumentGuard.NotNull(serializer, nameof(serializer)); + ArgumentGuard.NotNull(request, nameof(request)); + ArgumentGuard.NotNull(responseModelAdapter, nameof(responseModelAdapter)); ArgumentGuard.NotNull(exceptionHandler, nameof(exceptionHandler)); ArgumentGuard.NotNull(eTagGenerator, nameof(eTagGenerator)); + ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); - _serializer = serializer; + _request = request; + _options = options; + _responseModelAdapter = responseModelAdapter; _exceptionHandler = exceptionHandler; _eTagGenerator = eTagGenerator; _traceWriter = new TraceLogWriter(loggerFactory); @@ -44,83 +53,84 @@ public async Task WriteAsync(object model, HttpContext httpContext) { ArgumentGuard.NotNull(httpContext, nameof(httpContext)); - HttpRequest request = httpContext.Request; - HttpResponse response = httpContext.Response; + if (model == null && !CanWriteBody((HttpStatusCode)httpContext.Response.StatusCode)) + { + // Prevent exception from Kestrel server, caused by writing data:null json response. + return; + } - await using TextWriter writer = new HttpResponseStreamWriter(response.Body, Encoding.UTF8); - string responseContent; + string responseBody = GetResponseBody(model, httpContext); + + _traceWriter.LogMessage(() => + $"Sending {httpContext.Response.StatusCode} response for {httpContext.Request.Method} request at '{httpContext.Request.GetEncodedUrl()}' with body: <<{responseBody}>>"); - using (IDisposable _ = CodeTimingSessionManager.Current.Measure("Write response body")) + await SendResponseBodyAsync(httpContext.Response, responseBody); + } + + private static bool CanWriteBody(HttpStatusCode statusCode) + { + return statusCode is not HttpStatusCode.NoContent and not HttpStatusCode.ResetContent and not HttpStatusCode.NotModified; + } + + private string GetResponseBody(object model, HttpContext httpContext) + { + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Write response body"); + + try { - try + if (model is ProblemDetails problemDetails) { - responseContent = SerializeResponse(model, (HttpStatusCode)response.StatusCode); + throw new UnsuccessfulActionResultException(problemDetails); } -#pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException - catch (Exception exception) -#pragma warning restore AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException + + if (model == null && !IsSuccessStatusCode((HttpStatusCode)httpContext.Response.StatusCode)) { - IReadOnlyList errors = _exceptionHandler.HandleException(exception); - responseContent = _serializer.Serialize(errors); - response.StatusCode = (int)ErrorObject.GetResponseStatusCode(errors); + throw new UnsuccessfulActionResultException((HttpStatusCode)httpContext.Response.StatusCode); } - bool hasMatchingETag = SetETagResponseHeader(request, response, responseContent); + string responseBody = RenderModel(model); - if (hasMatchingETag) + if (SetETagResponseHeader(httpContext.Request, httpContext.Response, responseBody)) { - response.StatusCode = (int)HttpStatusCode.NotModified; - responseContent = string.Empty; + httpContext.Response.StatusCode = (int)HttpStatusCode.NotModified; + return null; } - if (request.Method == HttpMethod.Head.Method) + if (httpContext.Request.Method == HttpMethod.Head.Method) { - responseContent = string.Empty; + return null; } - if (!string.IsNullOrEmpty(responseContent)) - { - response.ContentType = _serializer.ContentType; - } + return responseBody; } - - _traceWriter.LogMessage(() => - $"Sending {response.StatusCode} response for {request.Method} request at '{request.GetEncodedUrl()}' with body: <<{responseContent}>>"); - - using (IDisposable _ = CodeTimingSessionManager.Current.Measure("Send response body")) +#pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException + catch (Exception exception) +#pragma warning restore AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException { - await writer.WriteAsync(responseContent); - await writer.FlushAsync(); + IReadOnlyList errors = _exceptionHandler.HandleException(exception); + httpContext.Response.StatusCode = (int)ErrorObject.GetResponseStatusCode(errors); + + return RenderModel(errors); } } - private string SerializeResponse(object contextObject, HttpStatusCode statusCode) + private static bool IsSuccessStatusCode(HttpStatusCode statusCode) { - if (contextObject is ProblemDetails problemDetails) - { - throw new UnsuccessfulActionResultException(problemDetails); - } - - if (contextObject == null) - { - if (!IsSuccessStatusCode(statusCode)) - { - throw new UnsuccessfulActionResultException(statusCode); - } - - if (statusCode is HttpStatusCode.NoContent or HttpStatusCode.ResetContent or HttpStatusCode.NotModified) - { - // Prevent exception from Kestrel server, caused by writing data:null json response. - return null; - } - } + return new HttpResponseMessage(statusCode).IsSuccessStatusCode; + } - return _serializer.Serialize(contextObject); + private string RenderModel(object model) + { + Document document = _responseModelAdapter.Convert(model); + return SerializeDocument(document); } - private bool IsSuccessStatusCode(HttpStatusCode statusCode) + private string SerializeDocument(Document document) { - return new HttpResponseMessage(statusCode).IsSuccessStatusCode; + using IDisposable _ = + CodeTimingSessionManager.Current.Measure("JsonSerializer.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); + + return JsonSerializer.Serialize(document, _options.SerializerWriteOptions); } private bool SetETagResponseHeader(HttpRequest request, HttpResponse response, string responseContent) @@ -159,5 +169,20 @@ private static bool RequestContainsMatchingETag(IHeaderDictionary requestHeaders return false; } + + private async Task SendResponseBodyAsync(HttpResponse httpResponse, string responseBody) + { + if (!string.IsNullOrEmpty(responseBody)) + { + httpResponse.ContentType = + _request.Kind == EndpointKind.AtomicOperations ? HeaderConstants.AtomicOperationsMediaType : HeaderConstants.MediaType; + + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Send response body"); + + await using TextWriter writer = new HttpResponseStreamWriter(httpResponse.Body, Encoding.UTF8); + await writer.WriteAsync(responseBody); + await writer.FlushAsync(); + } + } } } diff --git a/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs index b6ca115495..24a9d85073 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs @@ -58,7 +58,7 @@ public ResponseModelAdapter(IJsonApiRequest request, IJsonApiOptions options, IR } /// - public (Document responseDocument, string contentType) Convert(object model) + public Document Convert(object model) { _sparseFieldSetCache.Reset(); @@ -108,8 +108,7 @@ public ResponseModelAdapter(IJsonApiRequest request, IJsonApiOptions options, IR document.Meta = _metaBuilder.Build(); document.Included = GetIncluded(includedCollection); - string contentType = _request.Kind == EndpointKind.AtomicOperations ? HeaderConstants.AtomicOperationsMediaType : HeaderConstants.MediaType; - return (document, contentType); + return document; } private AtomicResultObject ConvertOperation(OperationContainer operation, IImmutableList includeElements, diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs index f795e429fd..9bb3b308bc 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs @@ -37,9 +37,6 @@ public class ResponseSerializer : BaseSerializer, IJsonApiSerializer private readonly IJsonApiRequest _request; private readonly Type _primaryResourceType; - /// - public string ContentType { get; } = HeaderConstants.MediaType; - public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, IFieldsToSerialize fieldsToSerialize, IResourceObjectBuilder resourceObjectBuilder, IResourceDefinitionAccessor resourceDefinitionAccessor, ISparseFieldSetCache sparseFieldSetCache, IJsonApiOptions options, IJsonApiRequest request) diff --git a/src/JsonApiDotNetCore/Serialization/TemporarySerializerBridge.cs b/src/JsonApiDotNetCore/Serialization/TemporarySerializerBridge.cs index af649283b5..0cc5112c9e 100644 --- a/src/JsonApiDotNetCore/Serialization/TemporarySerializerBridge.cs +++ b/src/JsonApiDotNetCore/Serialization/TemporarySerializerBridge.cs @@ -16,8 +16,6 @@ public sealed class TemporarySerializerBridge : IJsonApiSerializer private readonly IJsonApiSerializerFactory _factory; private readonly IJsonApiOptions _options; - public string ContentType { get; private set; } - public TemporarySerializerBridge(IResponseModelAdapter responseModelAdapter, IJsonApiSerializerFactory factory, IJsonApiOptions options) { ArgumentGuard.NotNull(responseModelAdapter, nameof(responseModelAdapter)); @@ -34,14 +32,10 @@ public string Serialize(object model) if (UseLegacySerializer(model)) { IJsonApiSerializer serializer = _factory.GetSerializer(); - string responseBody = serializer.Serialize(model); - ContentType = serializer.ContentType; - return responseBody; + return serializer.Serialize(model); } - (Document document, string contentType) = _responseModelAdapter.Convert(model); - ContentType = contentType; - + Document document = _responseModelAdapter.Convert(model); return SerializeObject(document, _options.SerializerWriteOptions); } From 428b06b12bfa5457d6a85b2dff9edc78c2ccdc10 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Oct 2021 15:02:00 +0200 Subject: [PATCH 32/49] Removed old code --- .../OperationsSerializationBenchmarks.cs | 6 - .../ResourceSerializationBenchmarks.cs | 6 - .../SerializationBenchmarkBase.cs | 25 +- .../JsonApiApplicationBuilder.cs | 18 +- .../AtomicOperationsResponseSerializer.cs | 152 ------ .../Serialization/BaseSerializer.cs | 98 ---- .../IIncludedResourceObjectBuilder.cs | 21 - .../Building/IResourceObjectBuilder.cs | 31 -- .../Building/IncludedResourceObjectBuilder.cs | 228 --------- .../Building/ResourceIdentityComparer.cs | 35 -- .../Building/ResourceObjectBuilder.cs | 181 ------- .../Building/ResponseResourceObjectBuilder.cs | 143 ------ .../Serialization/FieldsToSerialize.cs | 84 ---- .../Serialization/IFieldsToSerialize.cs | 28 -- .../Serialization/IJsonApiSerializer.cs | 13 - .../IJsonApiSerializerFactory.cs | 13 - .../Serialization/ResponseSerializer.cs | 196 -------- .../ResponseSerializerFactory.cs | 51 -- .../TemporarySerializerBridge.cs | 55 --- .../Common/BaseDocumentBuilderTests.cs | 81 ---- .../Common/ResourceObjectBuilderTests.cs | 235 --------- .../FakeRequestQueryStringAccessor.cs | 21 - .../SerializationTestsSetupBase.cs | 93 ---- .../Serialization/SerializerTestsSetup.cs | 198 -------- .../IncludedResourceObjectBuilderTests.cs | 221 --------- .../ResponseResourceObjectBuilderTests.cs | 122 ----- .../Server/ResponseSerializerTests.cs | 450 ------------------ wiki/v4/content/serialization.md | 111 ----- 28 files changed, 7 insertions(+), 2909 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/BaseSerializer.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Building/IIncludedResourceObjectBuilder.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilder.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Building/ResourceIdentityComparer.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/IJsonApiSerializerFactory.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs delete mode 100644 src/JsonApiDotNetCore/Serialization/TemporarySerializerBridge.cs delete mode 100644 test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs delete mode 100644 test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs delete mode 100644 test/UnitTests/Serialization/FakeRequestQueryStringAccessor.cs delete mode 100644 test/UnitTests/Serialization/SerializationTestsSetupBase.cs delete mode 100644 test/UnitTests/Serialization/SerializerTestsSetup.cs delete mode 100644 test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs delete mode 100644 test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs delete mode 100644 test/UnitTests/Serialization/Server/ResponseSerializerTests.cs delete mode 100644 wiki/v4/content/serialization.md diff --git a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs index ac10ed3cfe..49f25622c5 100644 --- a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs +++ b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs @@ -120,12 +120,6 @@ public string SerializeOperationsResponse() return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions); } - [Benchmark] - public string LegacySerializeOperationsResponse() - { - return JsonApiOperationsSerializer.Serialize(_responseOperations); - } - protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) { return new JsonApiRequest diff --git a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs index 0271e2abc1..5237ad2642 100644 --- a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs +++ b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs @@ -111,12 +111,6 @@ public string SerializeResourceResponse() return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions); } - [Benchmark] - public string LegacySerializeResourceResponse() - { - return JsonApiResourceSerializer.Serialize(ResponseResource); - } - protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) { return new JsonApiRequest diff --git a/benchmarks/Serialization/SerializationBenchmarkBase.cs b/benchmarks/Serialization/SerializationBenchmarkBase.cs index 41e0c1c1e9..3846720bbc 100644 --- a/benchmarks/Serialization/SerializationBenchmarkBase.cs +++ b/benchmarks/Serialization/SerializationBenchmarkBase.cs @@ -26,8 +26,6 @@ public abstract class SerializationBenchmarkBase { protected readonly JsonSerializerOptions SerializerWriteOptions; protected readonly IResponseModelAdapter ResponseModelAdapter; - protected readonly IJsonApiSerializer JsonApiResourceSerializer; - protected readonly IJsonApiSerializer JsonApiOperationsSerializer; protected readonly IResourceGraph ResourceGraph; protected SerializationBenchmarkBase() @@ -58,27 +56,8 @@ protected SerializationBenchmarkBase() var sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); var requestQueryStringAccessor = new FakeRequestQueryStringAccessor(); - { - IFieldsToSerialize fieldsToSerialize = - new FieldsToSerialize(ResourceGraph, constraintProviders, resourceDefinitionAccessor, request, sparseFieldSetCache); - - IIncludedResourceObjectBuilder includeBuilder = new IncludedResourceObjectBuilder(fieldsToSerialize, linkBuilder, ResourceGraph, - constraintProviders, resourceDefinitionAccessor, requestQueryStringAccessor, options, sparseFieldSetCache); - - var resourceObjectBuilder = new ResponseResourceObjectBuilder(linkBuilder, includeBuilder, constraintProviders, ResourceGraph, - resourceDefinitionAccessor, options, evaluatedIncludeCache, sparseFieldSetCache); - - JsonApiResourceSerializer = new ResponseSerializer(metaBuilder, linkBuilder, includeBuilder, fieldsToSerialize, - resourceObjectBuilder, resourceDefinitionAccessor, sparseFieldSetCache, options, request); - - JsonApiOperationsSerializer = new AtomicOperationsResponseSerializer(resourceObjectBuilder, metaBuilder, linkBuilder, fieldsToSerialize, - resourceDefinitionAccessor, evaluatedIncludeCache, sparseFieldSetCache, request, options); - } - - { - ResponseModelAdapter = new ResponseModelAdapter(request, options, ResourceGraph, linkBuilder, metaBuilder, resourceDefinitionAccessor, - evaluatedIncludeCache, sparseFieldSetCache, requestQueryStringAccessor); - } + ResponseModelAdapter = new ResponseModelAdapter(request, options, ResourceGraph, linkBuilder, metaBuilder, resourceDefinitionAccessor, + evaluatedIncludeCache, sparseFieldSetCache, requestQueryStringAccessor); } protected abstract JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 171bc90d2e..fc428df3d1 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -180,7 +180,6 @@ private void AddMiddlewareLayer() _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(); } private void AddResourceLayer() @@ -252,17 +251,6 @@ private void RegisterDependentService() private void AddSerializationLayer() { - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(typeof(ResponseSerializer<>)); - _services.AddScoped(typeof(AtomicOperationsResponseSerializer)); - _services.AddScoped(); - _services.AddSingleton(); - _services.AddSingleton(); - _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); @@ -274,8 +262,12 @@ private void AddSerializationLayer() _services.AddScoped(); _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddSingleton(); + _services.AddSingleton(); _services.AddScoped(); - _services.AddScoped(); } private void AddOperationsLayer() diff --git a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs deleted file mode 100644 index 84f388b46a..0000000000 --- a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Queries.Internal; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// Server serializer implementation of for atomic:operations responses. - /// - [PublicAPI] - public sealed class AtomicOperationsResponseSerializer : BaseSerializer, IJsonApiSerializer - { - private readonly IMetaBuilder _metaBuilder; - private readonly ILinkBuilder _linkBuilder; - private readonly IFieldsToSerialize _fieldsToSerialize; - private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; - private readonly IEvaluatedIncludeCache _evaluatedIncludeCache; - private readonly ISparseFieldSetCache _sparseFieldSetCache; - private readonly IJsonApiRequest _request; - private readonly IJsonApiOptions _options; - - public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectBuilder, IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, - IFieldsToSerialize fieldsToSerialize, IResourceDefinitionAccessor resourceDefinitionAccessor, IEvaluatedIncludeCache evaluatedIncludeCache, - ISparseFieldSetCache sparseFieldSetCache, IJsonApiRequest request, IJsonApiOptions options) - : base(resourceObjectBuilder) - { - ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder)); - ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); - ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); - ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); - ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache)); - ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); - ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(options, nameof(options)); - - _metaBuilder = metaBuilder; - _linkBuilder = linkBuilder; - _fieldsToSerialize = fieldsToSerialize; - _resourceDefinitionAccessor = resourceDefinitionAccessor; - _evaluatedIncludeCache = evaluatedIncludeCache; - _sparseFieldSetCache = sparseFieldSetCache; - _request = request; - _options = options; - } - - /// - public string Serialize(object content) - { - if (content is IEnumerable operations) - { - return SerializeOperationsDocument(operations); - } - - if (content is IEnumerable errors) - { - var errorDocument = new Document - { - Errors = errors.ToArray() - }; - - return SerializeErrorDocument(errorDocument); - } - - if (content is ErrorObject errorObject) - { - var errorDocument = new Document - { - Errors = errorObject.AsArray() - }; - - return SerializeErrorDocument(errorDocument); - } - - throw new InvalidOperationException("Data being returned must be errors or operations."); - } - - private string SerializeOperationsDocument(IEnumerable operations) - { - var document = new Document - { - Results = operations.Select(SerializeOperation).ToList(), - Meta = _metaBuilder.Build(), - Links = _linkBuilder.GetTopLevelLinks() - }; - - SetApiVersion(document); - - return SerializeObject(document, _options.SerializerWriteOptions); - } - - private void SetApiVersion(Document document) - { - if (_options.IncludeJsonApiVersion) - { - document.JsonApi = new JsonApiObject - { - Version = "1.1", - Ext = new List - { - "https://jsonapi.org/ext/atomic" - } - }; - } - } - - private AtomicResultObject SerializeOperation(OperationContainer operation) - { - ResourceObject resourceObject = null; - - if (operation != null) - { - _request.CopyFrom(operation.Request); - _sparseFieldSetCache.Reset(); - _evaluatedIncludeCache.Set(null); - - _resourceDefinitionAccessor.OnSerialize(operation.Resource); - - Type resourceType = operation.Resource.GetType(); - IReadOnlyCollection attributes = _fieldsToSerialize.GetAttributes(resourceType); - IReadOnlyCollection relationships = _fieldsToSerialize.GetRelationships(resourceType); - - resourceObject = ResourceObjectBuilder.Build(operation.Resource, attributes, relationships); - } - - if (resourceObject != null && operation.Request.Kind != EndpointKind.Relationship) - { - resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); - } - - return new AtomicResultObject - { - Data = new SingleOrManyData(resourceObject) - }; - } - - private string SerializeErrorDocument(Document document) - { - document.Links = _linkBuilder.GetTopLevelLinks(); - SetApiVersion(document); - - return SerializeObject(document, _options.SerializerWriteOptions); - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs deleted file mode 100644 index b6bfc14b85..0000000000 --- a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using JsonApiDotNetCore.Diagnostics; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// Abstract base class for serialization. Uses to convert resources into s and wraps - /// them in a . - /// - public abstract class BaseSerializer - { - protected IResourceObjectBuilder ResourceObjectBuilder { get; } - - protected BaseSerializer(IResourceObjectBuilder resourceObjectBuilder) - { - ArgumentGuard.NotNull(resourceObjectBuilder, nameof(resourceObjectBuilder)); - - ResourceObjectBuilder = resourceObjectBuilder; - } - - /// - /// Builds a for . Adds the attributes and relationships that are enlisted in - /// and . - /// - /// - /// Resource to build a for. - /// - /// - /// Attributes to include in the building process. - /// - /// - /// Relationships to include in the building process. - /// - /// - /// The resource object that was built. - /// - protected Document Build(IIdentifiable resource, IReadOnlyCollection attributes, - IReadOnlyCollection relationships) - { - ResourceObject resourceObject = resource != null ? ResourceObjectBuilder.Build(resource, attributes, relationships) : null; - - return new Document - { - Data = new SingleOrManyData(resourceObject) - }; - } - - /// - /// Builds a for . Adds the attributes and relationships that are enlisted in - /// and . - /// - /// - /// Resource to build a for. - /// - /// - /// Attributes to include in the building process. - /// - /// - /// Relationships to include in the building process. - /// - /// - /// The resource object that was built. - /// - protected Document Build(IReadOnlyCollection resources, IReadOnlyCollection attributes, - IReadOnlyCollection relationships) - { - ArgumentGuard.NotNull(resources, nameof(resources)); - - var resourceObjects = new List(); - - foreach (IIdentifiable resource in resources) - { - resourceObjects.Add(ResourceObjectBuilder.Build(resource, attributes, relationships)); - } - - return new Document - { - Data = new SingleOrManyData(resourceObjects) - }; - } - - protected string SerializeObject(object value, JsonSerializerOptions serializerOptions) - { - ArgumentGuard.NotNull(serializerOptions, nameof(serializerOptions)); - - using IDisposable _ = - CodeTimingSessionManager.Current.Measure("JsonSerializer.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - - return JsonSerializer.Serialize(value, serializerOptions); - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Building/IIncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IIncludedResourceObjectBuilder.cs deleted file mode 100644 index 3a49f0d413..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Building/IIncludedResourceObjectBuilder.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization.Building -{ - public interface IIncludedResourceObjectBuilder - { - /// - /// Gets the list of resource objects representing the included resources. - /// - IList Build(); - - /// - /// Extracts the included resources from using the (arbitrarily deeply nested) included relationships in - /// . - /// - void IncludeRelationshipChain(IReadOnlyCollection inclusionChain, IIdentifiable rootResource); - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilder.cs deleted file mode 100644 index ff182c2dab..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilder.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization.Building -{ - /// - /// Responsible for converting resources into s given a collection of attributes and relationships. - /// - public interface IResourceObjectBuilder - { - /// - /// Converts into a . Adds the attributes and relationships that are enlisted in - /// and . - /// - /// - /// Resource to build a for. - /// - /// - /// Attributes to include in the building process. - /// - /// - /// Relationships to include in the building process. - /// - /// - /// The resource object that was built. - /// - ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes, IReadOnlyCollection relationships); - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs deleted file mode 100644 index 5dad7eaad9..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Internal; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization.Building -{ - [PublicAPI] - public class IncludedResourceObjectBuilder : ResourceObjectBuilder, IIncludedResourceObjectBuilder - { - private readonly HashSet _included; - private readonly IFieldsToSerialize _fieldsToSerialize; - private readonly ILinkBuilder _linkBuilder; - private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; - private readonly IRequestQueryStringAccessor _queryStringAccessor; - private readonly ISparseFieldSetCache _sparseFieldSetCache; - - public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILinkBuilder linkBuilder, IResourceGraph resourceGraph, - IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, - IRequestQueryStringAccessor queryStringAccessor, IJsonApiOptions options, ISparseFieldSetCache sparseFieldSetCache) - : base(resourceGraph, options) - { - ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); - ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); - ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); - ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); - ArgumentGuard.NotNull(queryStringAccessor, nameof(queryStringAccessor)); - ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); - - _included = new HashSet(ResourceIdentityComparer.Instance); - _fieldsToSerialize = fieldsToSerialize; - _linkBuilder = linkBuilder; - _resourceDefinitionAccessor = resourceDefinitionAccessor; - _queryStringAccessor = queryStringAccessor; - _sparseFieldSetCache = sparseFieldSetCache; - } - - /// - public IList Build() - { - if (_included.Any()) - { - // Cleans relationship dictionaries and adds links of resources. - foreach (ResourceObject resourceObject in _included) - { - if (resourceObject.Relationships != null) - { - UpdateRelationships(resourceObject); - } - - resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); - } - - return _included.ToArray(); - } - - return _queryStringAccessor.Query.ContainsKey("include") ? Array.Empty() : null; - } - - private void UpdateRelationships(ResourceObject resourceObject) - { - foreach (string relationshipName in resourceObject.Relationships.Keys) - { - ResourceContext resourceContext = ResourceGraph.GetResourceContext(resourceObject.Type); - RelationshipAttribute relationship = resourceContext.GetRelationshipByPublicName(relationshipName); - - if (!IsRelationshipInSparseFieldSet(relationship)) - { - resourceObject.Relationships.Remove(relationshipName); - } - } - - resourceObject.Relationships = PruneRelationshipObjects(resourceObject); - } - - private static IDictionary PruneRelationshipObjects(ResourceObject resourceObject) - { - 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; - } - - private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) - { - ResourceContext resourceContext = ResourceGraph.GetResourceContext(relationship.LeftType); - - IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); - return fieldSet.Contains(relationship); - } - - /// - public override ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, - IReadOnlyCollection relationships = null) - { - ResourceObject resourceObject = base.Build(resource, attributes, relationships); - - resourceObject.Meta = _resourceDefinitionAccessor.GetMeta(resource.GetType(), resource); - - return resourceObject; - } - - /// - public void IncludeRelationshipChain(IReadOnlyCollection inclusionChain, IIdentifiable rootResource) - { - ArgumentGuard.NotNull(inclusionChain, nameof(inclusionChain)); - ArgumentGuard.NotNull(rootResource, nameof(rootResource)); - - // We don't have to build a resource object for the root resource because - // this one is already encoded in the documents primary data, so we process the chain - // starting from the first related resource. - RelationshipAttribute relationship = inclusionChain.First(); - IList chainRemainder = ShiftChain(inclusionChain); - object related = relationship.GetValue(rootResource); - ProcessChain(related, chainRemainder); - } - - private void ProcessChain(object related, IList inclusionChain) - { - if (related is IEnumerable children) - { - foreach (IIdentifiable child in children) - { - ProcessRelationship(child, inclusionChain); - } - } - else - { - ProcessRelationship((IIdentifiable)related, inclusionChain); - } - } - - private void ProcessRelationship(IIdentifiable parent, IList inclusionChain) - { - if (parent == null) - { - return; - } - - ResourceObject resourceObject = TryGetBuiltResourceObjectFor(parent); - - if (resourceObject == null) - { - _resourceDefinitionAccessor.OnSerialize(parent); - - resourceObject = BuildCachedResourceObjectFor(parent); - } - - if (!inclusionChain.Any()) - { - return; - } - - RelationshipAttribute nextRelationship = inclusionChain.First(); - List chainRemainder = inclusionChain.ToList(); - chainRemainder.RemoveAt(0); - - string nextRelationshipName = nextRelationship.PublicName; - IDictionary relationshipsObject = resourceObject.Relationships; - - if (!relationshipsObject.TryGetValue(nextRelationshipName, out RelationshipObject relationshipObject)) - { - relationshipObject = GetRelationshipData(nextRelationship, parent); - relationshipsObject[nextRelationshipName] = relationshipObject; - } - - relationshipObject.Data = GetRelatedResourceLinkage(nextRelationship, parent); - - if (relationshipObject.Data.IsAssigned && relationshipObject.Data.Value != null) - { - // if the relationship is set, continue parsing the chain. - object related = nextRelationship.GetValue(parent); - ProcessChain(related, chainRemainder); - } - } - - private IList ShiftChain(IReadOnlyCollection chain) - { - List chainRemainder = chain.ToList(); - chainRemainder.RemoveAt(0); - return chainRemainder; - } - - /// - /// We only need an empty relationship object here. It will be populated in the ProcessRelationships method. - /// - protected override RelationshipObject GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) - { - ArgumentGuard.NotNull(relationship, nameof(relationship)); - ArgumentGuard.NotNull(resource, nameof(resource)); - - return new RelationshipObject - { - Links = _linkBuilder.GetRelationshipLinks(relationship, resource) - }; - } - - private ResourceObject TryGetBuiltResourceObjectFor(IIdentifiable resource) - { - Type resourceType = resource.GetType(); - ResourceContext resourceContext = ResourceGraph.GetResourceContext(resourceType); - - return _included.SingleOrDefault(resourceObject => resourceObject.Type == resourceContext.PublicName && resourceObject.Id == resource.StringId); - } - - private ResourceObject BuildCachedResourceObjectFor(IIdentifiable resource) - { - Type resourceType = resource.GetType(); - IReadOnlyCollection attributes = _fieldsToSerialize.GetAttributes(resourceType); - IReadOnlyCollection relationships = _fieldsToSerialize.GetRelationships(resourceType); - - ResourceObject resourceObject = Build(resource, attributes, relationships); - - _included.Add(resourceObject); - - return resourceObject; - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentityComparer.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentityComparer.cs deleted file mode 100644 index 722008815e..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentityComparer.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization.Building -{ - internal sealed class ResourceIdentityComparer : IEqualityComparer - { - public static readonly ResourceIdentityComparer Instance = new(); - - private ResourceIdentityComparer() - { - } - - public bool Equals(IResourceIdentity x, IResourceIdentity y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null || y is null || x.GetType() != y.GetType()) - { - return false; - } - - return x.Type == y.Type && x.Id == y.Id && x.Lid == y.Lid; - } - - 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 deleted file mode 100644 index 6f78367108..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs +++ /dev/null @@ -1,181 +0,0 @@ -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; - -namespace JsonApiDotNetCore.Serialization.Building -{ - /// - [PublicAPI] - public class ResourceObjectBuilder : IResourceObjectBuilder - { - private static readonly CollectionConverter CollectionConverter = new(); - private readonly IJsonApiOptions _options; - - protected IResourceGraph ResourceGraph { get; } - - public ResourceObjectBuilder(IResourceGraph resourceGraph, IJsonApiOptions options) - { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - ArgumentGuard.NotNull(options, nameof(options)); - - ResourceGraph = resourceGraph; - _options = options; - } - - /// - public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, - IReadOnlyCollection relationships = null) - { - ArgumentGuard.NotNull(resource, nameof(resource)); - - ResourceContext resourceContext = ResourceGraph.GetResourceContext(resource.GetType()); - - // populating the top-level "type" and "id" members. - var resourceObject = new ResourceObject - { - Type = resourceContext.PublicName, - Id = resource.StringId - }; - - // populating the top-level "attribute" member of a resource object. never include "id" as an attribute - if (attributes != null) - { - AttrAttribute[] attributesWithoutId = attributes.Where(attr => attr.Property.Name != nameof(Identifiable.Id)).ToArray(); - - if (attributesWithoutId.Any()) - { - ProcessAttributes(resource, attributesWithoutId, resourceObject); - } - } - - // populating the top-level "relationship" member of a resource object. - if (relationships != null) - { - ProcessRelationships(resource, relationships, resourceObject); - } - - return resourceObject; - } - - /// - /// Builds a . The default behavior is to just construct a resource linkage with the "data" field populated with - /// "single" or "many" data. - /// - protected virtual RelationshipObject GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) - { - ArgumentGuard.NotNull(relationship, nameof(relationship)); - ArgumentGuard.NotNull(resource, nameof(resource)); - - return new RelationshipObject - { - Data = GetRelatedResourceLinkage(relationship, resource) - }; - } - - /// - /// Gets the value for the data property. - /// - protected SingleOrManyData GetRelatedResourceLinkage(RelationshipAttribute relationship, IIdentifiable resource) - { - ArgumentGuard.NotNull(relationship, nameof(relationship)); - ArgumentGuard.NotNull(resource, nameof(resource)); - - return relationship is HasOneAttribute hasOne - ? GetRelatedResourceLinkageForHasOne(hasOne, resource) - : GetRelatedResourceLinkageForHasMany((HasManyAttribute)relationship, resource); - } - - /// - /// Builds a for a HasOne relationship. - /// - private SingleOrManyData GetRelatedResourceLinkageForHasOne(HasOneAttribute relationship, IIdentifiable resource) - { - var relatedResource = (IIdentifiable)relationship.GetValue(resource); - ResourceIdentifierObject resourceIdentifierObject = relatedResource != null ? GetResourceIdentifier(relatedResource) : null; - return new SingleOrManyData(resourceIdentifierObject); - } - - /// - /// Builds the s for a HasMany relationship. - /// - private SingleOrManyData GetRelatedResourceLinkageForHasMany(HasManyAttribute relationship, IIdentifiable resource) - { - object value = relationship.GetValue(resource); - ICollection relatedResources = CollectionConverter.ExtractResources(value); - - var manyData = new List(); - - if (relatedResources != null) - { - foreach (IIdentifiable relatedResource in relatedResources) - { - manyData.Add(GetResourceIdentifier(relatedResource)); - } - } - - return new SingleOrManyData(manyData); - } - - /// - /// Creates a from . - /// - private ResourceIdentifierObject GetResourceIdentifier(IIdentifiable resource) - { - string publicName = ResourceGraph.GetResourceContext(resource.GetType()).PublicName; - - return new ResourceIdentifierObject - { - Type = publicName, - Id = resource.StringId - }; - } - - /// - /// Puts the relationships of the resource into the resource object. - /// - private void ProcessRelationships(IIdentifiable resource, IEnumerable relationships, ResourceObject ro) - { - foreach (RelationshipAttribute rel in relationships) - { - RelationshipObject relData = GetRelationshipData(rel, resource); - - if (relData != null) - { - (ro.Relationships ??= new Dictionary()).Add(rel.PublicName, relData); - } - } - } - - /// - /// Puts the attributes of the resource into the resource object. - /// - private void ProcessAttributes(IIdentifiable resource, IEnumerable attributes, ResourceObject ro) - { - ro.Attributes = new Dictionary(); - - foreach (AttrAttribute attr in attributes) - { - object value = attr.GetValue(resource); - - if (_options.SerializerOptions.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull && value == null) - { - continue; - } - - if (_options.SerializerOptions.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingDefault && - Equals(value, RuntimeTypeConverter.GetDefaultValue(attr.Property.PropertyType))) - { - continue; - } - - ro.Attributes.Add(attr.PublicName, value); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs deleted file mode 100644 index 2f5e49fad3..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Queries.Internal; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization.Building -{ - [PublicAPI] - public class ResponseResourceObjectBuilder : ResourceObjectBuilder - { - private static readonly IncludeChainConverter IncludeChainConverter = new(); - - private readonly ILinkBuilder _linkBuilder; - private readonly IIncludedResourceObjectBuilder _includedBuilder; - private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; - private readonly IEvaluatedIncludeCache _evaluatedIncludeCache; - private readonly ISparseFieldSetCache _sparseFieldSetCache; - - private RelationshipAttribute _requestRelationship; - - public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, - IEnumerable constraintProviders, IResourceGraph resourceGraph, IResourceDefinitionAccessor resourceDefinitionAccessor, - IJsonApiOptions options, IEvaluatedIncludeCache evaluatedIncludeCache, ISparseFieldSetCache sparseFieldSetCache) - : base(resourceGraph, options) - { - ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); - ArgumentGuard.NotNull(includedBuilder, nameof(includedBuilder)); - ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); - ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); - ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache)); - ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); - - _linkBuilder = linkBuilder; - _includedBuilder = includedBuilder; - _resourceDefinitionAccessor = resourceDefinitionAccessor; - _evaluatedIncludeCache = evaluatedIncludeCache; - _sparseFieldSetCache = sparseFieldSetCache; - } - - public RelationshipObject Build(IIdentifiable resource, RelationshipAttribute requestRelationship) - { - ArgumentGuard.NotNull(resource, nameof(resource)); - ArgumentGuard.NotNull(requestRelationship, nameof(requestRelationship)); - - _requestRelationship = requestRelationship; - return GetRelationshipData(requestRelationship, resource); - } - - /// - public override ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, - IReadOnlyCollection relationships = null) - { - ResourceObject resourceObject = base.Build(resource, attributes, relationships); - - resourceObject.Meta = _resourceDefinitionAccessor.GetMeta(resource.GetType(), resource); - - return resourceObject; - } - - /// - /// 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 RelationshipObject GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) - { - ArgumentGuard.NotNull(relationship, nameof(relationship)); - ArgumentGuard.NotNull(resource, nameof(resource)); - - RelationshipObject relationshipObject = null; - IReadOnlyCollection> relationshipChains = GetInclusionChainsStartingWith(relationship); - - if (Equals(relationship, _requestRelationship) || relationshipChains.Any()) - { - relationshipObject = base.GetRelationshipData(relationship, resource); - - if (relationshipChains.Any() && relationshipObject.Data.Value != null) - { - foreach (IReadOnlyCollection chain in relationshipChains) - { - // traverses (recursively) and extracts all (nested) related resources for the current inclusion chain. - _includedBuilder.IncludeRelationshipChain(chain, resource); - } - } - } - - if (!IsRelationshipInSparseFieldSet(relationship)) - { - return null; - } - - RelationshipLinks links = _linkBuilder.GetRelationshipLinks(relationship, resource); - - if (links != null) - { - // 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 object from the output. - // (see the NullValueHandling settings on ) - return relationshipObject; - } - - private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) - { - ResourceContext resourceContext = ResourceGraph.GetResourceContext(relationship.LeftType); - - IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); - return fieldSet.Contains(relationship); - } - - /// - /// Inspects the included relationship chains and selects the ones that starts with the specified relationship. - /// - private IReadOnlyCollection> GetInclusionChainsStartingWith(RelationshipAttribute relationship) - { - IncludeExpression include = _evaluatedIncludeCache.Get() ?? IncludeExpression.Empty; - IReadOnlyCollection chains = IncludeChainConverter.GetRelationshipChains(include); - - var inclusionChains = new List>(); - - foreach (ResourceFieldChainExpression chain in chains) - { - if (chain.Fields[0].Equals(relationship)) - { - inclusionChains.Add(chain.Fields.Cast().ToArray()); - } - } - - return inclusionChains; - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs deleted file mode 100644 index 988f0dc8b1..0000000000 --- a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Internal; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Serialization -{ - /// - [PublicAPI] - public class FieldsToSerialize : IFieldsToSerialize - { - private readonly IResourceGraph _resourceGraph; - private readonly IJsonApiRequest _request; - private readonly ISparseFieldSetCache _sparseFieldSetCache; - - /// - public bool ShouldSerialize => _request.Kind != EndpointKind.Relationship; - - public FieldsToSerialize(IResourceGraph resourceGraph, IEnumerable constraintProviders, - IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiRequest request, ISparseFieldSetCache sparseFieldSetCache) - { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); - - _resourceGraph = resourceGraph; - _request = request; - _sparseFieldSetCache = sparseFieldSetCache; - } - - /// - public IReadOnlyCollection GetAttributes(Type resourceType) - { - ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - - if (!ShouldSerialize) - { - return Array.Empty(); - } - - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); - IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); - - return SortAttributesInDeclarationOrder(fieldSet, resourceContext).ToArray(); - } - - private IEnumerable SortAttributesInDeclarationOrder(IImmutableSet fieldSet, ResourceContext resourceContext) - { - foreach (AttrAttribute attribute in resourceContext.Attributes) - { - if (fieldSet.Contains(attribute)) - { - yield return attribute; - } - } - } - - /// - /// - /// Note: this method does NOT check if a relationship is included to determine if it should be serialized. This is because completely hiding a - /// relationship is not the same as not including. In the case of the latter, we may still want to add the relationship to expose the navigation link to - /// the client. - /// - public IReadOnlyCollection GetRelationships(Type resourceType) - { - ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - - if (!ShouldSerialize) - { - return Array.Empty(); - } - - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); - return resourceContext.Relationships; - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs deleted file mode 100644 index a55c923a56..0000000000 --- a/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// Responsible for getting the set of fields that are to be included for a given type in the serialization result. Typically combines various sources of - /// information, like application-wide and request-wide sparse fieldsets. - /// - public interface IFieldsToSerialize - { - /// - /// Indicates whether attributes and relationships should be serialized, based on the current endpoint. - /// - bool ShouldSerialize { get; } - - /// - /// Gets the collection of attributes that are to be serialized for resources of type . - /// - IReadOnlyCollection GetAttributes(Type resourceType); - - /// - /// Gets the collection of relationships that are to be serialized for resources of type . - /// - IReadOnlyCollection GetRelationships(Type resourceType); - } -} diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs deleted file mode 100644 index 29d09a2c36..0000000000 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace JsonApiDotNetCore.Serialization -{ - /// - /// Serializer used internally in JsonApiDotNetCore to serialize responses. - /// - public interface IJsonApiSerializer - { - /// - /// Serializes a single resource or a collection of resources. - /// - string Serialize(object content); - } -} diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializerFactory.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializerFactory.cs deleted file mode 100644 index 38796a596e..0000000000 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializerFactory.cs +++ /dev/null @@ -1,13 +0,0 @@ -using JetBrains.Annotations; - -namespace JsonApiDotNetCore.Serialization -{ - [PublicAPI] - public interface IJsonApiSerializerFactory - { - /// - /// Instantiates the serializer to process the servers response. - /// - IJsonApiSerializer GetSerializer(); - } -} diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs deleted file mode 100644 index 9bb3b308bc..0000000000 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Queries.Internal; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// Server serializer implementation of for resources of a specific type. - /// - /// - /// Because in JsonApiDotNetCore every JSON:API request is associated with exactly one resource (the primary resource, see - /// ), the serializer can leverage this information using generics. See - /// for how this is instantiated. - /// - /// - /// Type of the resource associated with the scope of the request for which this serializer is used. - /// - [PublicAPI] - public class ResponseSerializer : BaseSerializer, IJsonApiSerializer - where TResource : class, IIdentifiable - { - private readonly IMetaBuilder _metaBuilder; - private readonly ILinkBuilder _linkBuilder; - private readonly IIncludedResourceObjectBuilder _includedBuilder; - private readonly IFieldsToSerialize _fieldsToSerialize; - private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; - private readonly ISparseFieldSetCache _sparseFieldSetCache; - private readonly IJsonApiOptions _options; - private readonly IJsonApiRequest _request; - private readonly Type _primaryResourceType; - - public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, - IFieldsToSerialize fieldsToSerialize, IResourceObjectBuilder resourceObjectBuilder, IResourceDefinitionAccessor resourceDefinitionAccessor, - ISparseFieldSetCache sparseFieldSetCache, IJsonApiOptions options, IJsonApiRequest request) - : base(resourceObjectBuilder) - { - ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder)); - ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); - ArgumentGuard.NotNull(includedBuilder, nameof(includedBuilder)); - ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); - ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); - ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); - ArgumentGuard.NotNull(options, nameof(options)); - ArgumentGuard.NotNull(request, nameof(request)); - - _metaBuilder = metaBuilder; - _linkBuilder = linkBuilder; - _includedBuilder = includedBuilder; - _fieldsToSerialize = fieldsToSerialize; - _resourceDefinitionAccessor = resourceDefinitionAccessor; - _sparseFieldSetCache = sparseFieldSetCache; - _options = options; - _request = request; - _primaryResourceType = typeof(TResource); - } - - /// - public string Serialize(object content) - { - _sparseFieldSetCache.Reset(); - - if (content is null or IIdentifiable) - { - return SerializeSingle((IIdentifiable)content); - } - - if (content is IEnumerable collectionOfIdentifiable) - { - return SerializeMany(collectionOfIdentifiable.ToArray()); - } - - if (content is IEnumerable errorObjects) - { - var errorDocument = new Document - { - Errors = errorObjects.ToArray() - }; - - return SerializeErrorDocument(errorDocument); - } - - if (content is ErrorObject errorObject) - { - var errorDocument = new Document - { - Errors = errorObject.AsArray() - }; - - return SerializeErrorDocument(errorDocument); - } - - throw new InvalidOperationException("Data being returned must be resources, operations, errors or null."); - } - - private string SerializeErrorDocument(Document document) - { - document.Links = _linkBuilder.GetTopLevelLinks(); - SetApiVersion(document); - - return SerializeObject(document, _options.SerializerWriteOptions); - } - - /// - /// Converts a single resource into a serialized . - /// - /// - /// This method is internal instead of private for easier testability. - /// - internal string SerializeSingle(IIdentifiable resource) - { - if (resource != null && _fieldsToSerialize.ShouldSerialize) - { - _resourceDefinitionAccessor.OnSerialize(resource); - } - - IReadOnlyCollection attributes = _fieldsToSerialize.GetAttributes(_primaryResourceType); - IReadOnlyCollection relationships = _fieldsToSerialize.GetRelationships(_primaryResourceType); - - Document document = Build(resource, attributes, relationships); - ResourceObject resourceObject = document.Data.SingleValue; - - if (resourceObject != null && _request.Kind != EndpointKind.Relationship) - { - resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); - } - - AddTopLevelObjects(document); - - return SerializeObject(document, _options.SerializerWriteOptions); - } - - /// - /// Converts a collection of resources into a serialized . - /// - /// - /// This method is internal instead of private for easier testability. - /// - internal string SerializeMany(IReadOnlyCollection resources) - { - if (_fieldsToSerialize.ShouldSerialize) - { - foreach (IIdentifiable resource in resources) - { - _resourceDefinitionAccessor.OnSerialize(resource); - } - } - - IReadOnlyCollection attributes = _fieldsToSerialize.GetAttributes(_primaryResourceType); - IReadOnlyCollection relationships = _fieldsToSerialize.GetRelationships(_primaryResourceType); - - Document document = Build(resources, attributes, relationships); - - foreach (ResourceObject resourceObject in document.Data.ManyValue) - { - resourceObject.Links = _request.Kind != EndpointKind.Relationship - ? _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id) - : null; - } - - AddTopLevelObjects(document); - - 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) - { - document.JsonApi = new JsonApiObject - { - Version = "1.1" - }; - } - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs deleted file mode 100644 index 5ddc248a4d..0000000000 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Middleware; -using Microsoft.Extensions.DependencyInjection; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// A factory class to abstract away the initialization of the serializer from the ASP.NET Core formatter pipeline. - /// - [PublicAPI] - public class ResponseSerializerFactory : IJsonApiSerializerFactory - { - private readonly IServiceProvider _provider; - private readonly IJsonApiRequest _request; - - public ResponseSerializerFactory(IJsonApiRequest request, IRequestScopedServiceProvider provider) - { - ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(provider, nameof(provider)); - - _request = request; - _provider = provider; - } - - /// - /// Initializes the server serializer using the associated with the current request. - /// - public IJsonApiSerializer GetSerializer() - { - if (_request.Kind == EndpointKind.AtomicOperations) - { - return (IJsonApiSerializer)_provider.GetRequiredService(typeof(AtomicOperationsResponseSerializer)); - } - - Type targetType = GetDocumentType(); - - Type serializerType = typeof(ResponseSerializer<>).MakeGenericType(targetType); - object serializer = _provider.GetRequiredService(serializerType); - - return (IJsonApiSerializer)serializer; - } - - private Type GetDocumentType() - { - ResourceContext resourceContext = _request.SecondaryResource ?? _request.PrimaryResource; - return resourceContext.ResourceType; - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/TemporarySerializerBridge.cs b/src/JsonApiDotNetCore/Serialization/TemporarySerializerBridge.cs deleted file mode 100644 index 0cc5112c9e..0000000000 --- a/src/JsonApiDotNetCore/Serialization/TemporarySerializerBridge.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Text.Json; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Diagnostics; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// Acts as a bridge between the legacy and new response serialization implementation. To be removed in a future commit, but handy for the moment to - /// quickly toggle and see what breaks. - /// - public sealed class TemporarySerializerBridge : IJsonApiSerializer - { - private readonly IResponseModelAdapter _responseModelAdapter; - private readonly IJsonApiSerializerFactory _factory; - private readonly IJsonApiOptions _options; - - public TemporarySerializerBridge(IResponseModelAdapter responseModelAdapter, IJsonApiSerializerFactory factory, IJsonApiOptions options) - { - ArgumentGuard.NotNull(responseModelAdapter, nameof(responseModelAdapter)); - ArgumentGuard.NotNull(factory, nameof(factory)); - ArgumentGuard.NotNull(options, nameof(options)); - - _responseModelAdapter = responseModelAdapter; - _factory = factory; - _options = options; - } - - public string Serialize(object model) - { - if (UseLegacySerializer(model)) - { - IJsonApiSerializer serializer = _factory.GetSerializer(); - return serializer.Serialize(model); - } - - Document document = _responseModelAdapter.Convert(model); - return SerializeObject(document, _options.SerializerWriteOptions); - } - - private bool UseLegacySerializer(object model) - { - return false; - } - - private string SerializeObject(object value, JsonSerializerOptions serializerOptions) - { - using IDisposable _ = - CodeTimingSessionManager.Current.Measure("JsonSerializer.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - - return JsonSerializer.Serialize(value, serializerOptions); - } - } -} diff --git a/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs b/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs deleted file mode 100644 index 5e590a4e21..0000000000 --- a/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Objects; -using Moq; -using UnitTests.TestModels; -using Xunit; - -namespace UnitTests.Serialization.Common -{ - public sealed class BaseDocumentBuilderTests : SerializerTestsSetup - { - private readonly TestSerializer _builder; - - public BaseDocumentBuilderTests() - { - var mock = new Mock(); - - mock.Setup(builder => builder.Build(It.IsAny(), It.IsAny>(), - It.IsAny>())).Returns(new ResourceObject()); - - _builder = new TestSerializer(mock.Object); - } - - [Fact] - public void ResourceToDocument_NullResource_CanBuild() - { - // Act - Document document = _builder.PublicBuild((TestResource)null); - - // Assert - Assert.Null(document.Data.Value); - Assert.True(document.Data.IsAssigned); - } - - [Fact] - public void ResourceToDocument_EmptyList_CanBuild() - { - // Act - Document document = _builder.PublicBuild(new List()); - - // Assert - Assert.NotNull(document.Data.Value); - Assert.Empty(document.Data.ManyValue); - } - - [Fact] - public void ResourceToDocument_SingleResource_CanBuild() - { - // Arrange - IIdentifiable dummy = new DummyResource(); - - // Act - Document document = _builder.PublicBuild(dummy); - - // Assert - Assert.NotNull(document.Data.Value); - Assert.True(document.Data.IsAssigned); - } - - [Fact] - public void ResourceToDocument_ResourceList_CanBuild() - { - // Arrange - DummyResource[] resources = ArrayFactory.Create(new DummyResource(), new DummyResource()); - - // Act - Document document = _builder.PublicBuild(resources); - IList data = document.Data.ManyValue; - - // Assert - Assert.Equal(2, data.Count); - } - - private sealed class DummyResource : Identifiable - { - } - } -} diff --git a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs deleted file mode 100644 index 3e44774876..0000000000 --- a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Objects; -using UnitTests.TestModels; -using Xunit; - -namespace UnitTests.Serialization.Common -{ - public sealed class ResourceObjectBuilderTests : SerializerTestsSetup - { - private readonly ResourceObjectBuilder _builder; - - public ResourceObjectBuilderTests() - { - _builder = new ResourceObjectBuilder(ResourceGraph, new JsonApiOptions()); - } - - [Fact] - public void ResourceToResourceObject_EmptyResource_CanBuild() - { - // Arrange - var resource = new TestResource(); - - // Act - ResourceObject resourceObject = _builder.Build(resource); - - // Assert - Assert.Null(resourceObject.Attributes); - Assert.Null(resourceObject.Relationships); - Assert.Null(resourceObject.Id); - Assert.Equal("testResource", resourceObject.Type); - } - - [Fact] - public void ResourceToResourceObject_ResourceWithId_CanBuild() - { - // Arrange - var resource = new TestResource - { - Id = 1 - }; - - // Act - ResourceObject resourceObject = _builder.Build(resource); - - // Assert - Assert.Equal("1", resourceObject.Id); - Assert.Null(resourceObject.Attributes); - Assert.Null(resourceObject.Relationships); - Assert.Equal("testResource", resourceObject.Type); - } - - [Theory] - [InlineData(null, null)] - [InlineData("string field", 1)] - public void ResourceToResourceObject_ResourceWithIncludedAttrs_CanBuild(string stringFieldValue, int? intFieldValue) - { - // Arrange - var resource = new TestResource - { - StringField = stringFieldValue, - NullableIntField = intFieldValue - }; - - IReadOnlyCollection attrs = ResourceGraph.GetAttributes(tr => new - { - tr.StringField, - tr.NullableIntField - }); - - // Act - ResourceObject resourceObject = _builder.Build(resource, attrs); - - // Assert - Assert.NotNull(resourceObject.Attributes); - Assert.Equal(2, resourceObject.Attributes.Keys.Count); - Assert.Equal(stringFieldValue, resourceObject.Attributes["stringField"]); - Assert.Equal(intFieldValue, resourceObject.Attributes["nullableIntField"]); - } - - [Fact] - public void ResourceWithRelationshipsToResourceObject_EmptyResource_CanBuild() - { - // Arrange - var resource = new MultipleRelationshipsPrincipalPart(); - - // Act - ResourceObject resourceObject = _builder.Build(resource); - - // Assert - Assert.Null(resourceObject.Attributes); - Assert.Null(resourceObject.Relationships); - Assert.Null(resourceObject.Id); - Assert.Equal("multiPrincipals", resourceObject.Type); - } - - [Fact] - public void ResourceWithRelationshipsToResourceObject_ResourceWithId_CanBuild() - { - // Arrange - var resource = new MultipleRelationshipsPrincipalPart - { - PopulatedToOne = new OneToOneDependent - { - Id = 10 - } - }; - - // Act - ResourceObject resourceObject = _builder.Build(resource); - - // Assert - Assert.Null(resourceObject.Attributes); - Assert.Null(resourceObject.Relationships); - Assert.Null(resourceObject.Id); - Assert.Equal("multiPrincipals", resourceObject.Type); - } - - [Fact] - public void ResourceWithRelationshipsToResourceObject_WithIncludedRelationshipsAttributes_CanBuild() - { - // Arrange - var resource = new MultipleRelationshipsPrincipalPart - { - PopulatedToOne = new OneToOneDependent - { - Id = 10 - }, - PopulatedToManies = new HashSet - { - new() - { - Id = 20 - } - } - }; - - IReadOnlyCollection relationships = ResourceGraph.GetRelationships(tr => new - { - tr.PopulatedToManies, - tr.PopulatedToOne, - tr.EmptyToOne, - tr.EmptyToManies - }); - - // Act - ResourceObject resourceObject = _builder.Build(resource, relationships: relationships); - - // Assert - Assert.Equal(4, resourceObject.Relationships.Count); - 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); - IList populatedToManyData = resourceObject.Relationships["populatedToManies"].Data.ManyValue; - Assert.Single(populatedToManyData); - Assert.Equal("20", populatedToManyData.First().Id); - Assert.Equal("oneToManyDependents", populatedToManyData.First().Type); - } - - [Fact] - public void ResourceWithRelationshipsToResourceObject_DeviatingForeignKeyWhileRelationshipIncluded_IgnoresForeignKeyDuringBuild() - { - // Arrange - var resource = new OneToOneDependent - { - Principal = new OneToOnePrincipal - { - Id = 10 - }, - PrincipalId = 123 - }; - - IReadOnlyCollection relationships = ResourceGraph.GetRelationships(tr => tr.Principal); - - // Act - ResourceObject resourceObject = _builder.Build(resource, relationships: relationships); - - // Assert - Assert.Single(resourceObject.Relationships); - Assert.NotNull(resourceObject.Relationships["principal"].Data.Value); - ResourceIdentifierObject ro = resourceObject.Relationships["principal"].Data.SingleValue; - Assert.Equal("10", ro.Id); - } - - [Fact] - public void ResourceWithRelationshipsToResourceObject_DeviatingForeignKeyAndNoNavigationWhileRelationshipIncluded_IgnoresForeignKeyDuringBuild() - { - // Arrange - var resource = new OneToOneDependent - { - Principal = null, - PrincipalId = 123 - }; - - IReadOnlyCollection relationships = ResourceGraph.GetRelationships(tr => tr.Principal); - - // Act - ResourceObject resourceObject = _builder.Build(resource, relationships: relationships); - - // Assert - Assert.Null(resourceObject.Relationships["principal"].Data.Value); - } - - [Fact] - public void ResourceWithRequiredRelationshipsToResourceObject_DeviatingForeignKeyWhileRelationshipIncluded_IgnoresForeignKeyDuringBuild() - { - // Arrange - var resource = new OneToOneRequiredDependent - { - Principal = new OneToOnePrincipal - { - Id = 10 - }, - PrincipalId = 123 - }; - - IReadOnlyCollection relationships = ResourceGraph.GetRelationships(tr => tr.Principal); - - // Act - ResourceObject resourceObject = _builder.Build(resource, relationships: relationships); - - // Assert - Assert.Single(resourceObject.Relationships); - 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/FakeRequestQueryStringAccessor.cs b/test/UnitTests/Serialization/FakeRequestQueryStringAccessor.cs deleted file mode 100644 index a2cfc2bebb..0000000000 --- a/test/UnitTests/Serialization/FakeRequestQueryStringAccessor.cs +++ /dev/null @@ -1,21 +0,0 @@ -using JsonApiDotNetCore.QueryStrings; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.WebUtilities; - -namespace UnitTests.Serialization -{ - internal sealed class FakeRequestQueryStringAccessor : IRequestQueryStringAccessor - { - public IQueryCollection Query { get; } - - public FakeRequestQueryStringAccessor() - : this(null) - { - } - - public FakeRequestQueryStringAccessor(string queryString) - { - Query = string.IsNullOrEmpty(queryString) ? QueryCollection.Empty : new QueryCollection(QueryHelpers.ParseQuery(queryString)); - } - } -} diff --git a/test/UnitTests/Serialization/SerializationTestsSetupBase.cs b/test/UnitTests/Serialization/SerializationTestsSetupBase.cs deleted file mode 100644 index 1d30044321..0000000000 --- a/test/UnitTests/Serialization/SerializationTestsSetupBase.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Bogus; -using JsonApiDotNetCore.Configuration; -using Microsoft.Extensions.Logging.Abstractions; -using UnitTests.TestModels; -using Person = UnitTests.TestModels.Person; - -namespace UnitTests.Serialization -{ - public class SerializationTestsSetupBase - { - protected IResourceGraph ResourceGraph { get; } - protected Faker FoodFaker { get; } - protected Faker SongFaker { get; } - protected Faker
ArticleFaker { get; } - protected Faker BlogFaker { get; } - protected Faker PersonFaker { get; } - - protected SerializationTestsSetupBase() - { - ResourceGraph = BuildGraph(); - - // @formatter:wrap_chained_method_calls chop_always - // @formatter:keep_existing_linebreaks true - - ArticleFaker = new Faker
() - .RuleFor(article => article.Title, faker => faker.Hacker.Phrase()) - .RuleFor(article => article.Id, faker => faker.UniqueIndex + 1); - - PersonFaker = new Faker() - .RuleFor(person => person.Name, faker => faker.Person.FullName) - .RuleFor(person => person.Id, faker => faker.UniqueIndex + 1); - - BlogFaker = new Faker() - .RuleFor(blog => blog.Title, faker => faker.Hacker.Phrase()) - .RuleFor(blog => blog.Id, faker => faker.UniqueIndex + 1); - - SongFaker = new Faker() - .RuleFor(song => song.Title, faker => faker.Lorem.Sentence()) - .RuleFor(song => song.Id, faker => faker.UniqueIndex + 1); - - FoodFaker = new Faker() - .RuleFor(food => food.Dish, faker => faker.Lorem.Sentence()) - .RuleFor(food => food.Id, faker => faker.UniqueIndex + 1); - - // @formatter:wrap_chained_method_calls restore - // @formatter:keep_existing_linebreaks restore - } - - private IResourceGraph BuildGraph() - { - var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); - resourceGraphBuilder.Add("testResource"); - resourceGraphBuilder.Add("testResource-with-list"); - - BuildOneToOneRelationships(resourceGraphBuilder); - BuildOneToManyRelationships(resourceGraphBuilder); - BuildCollectiveRelationships(resourceGraphBuilder); - - resourceGraphBuilder.Add
(); - resourceGraphBuilder.Add(); - resourceGraphBuilder.Add(); - resourceGraphBuilder.Add(); - resourceGraphBuilder.Add(); - - resourceGraphBuilder.Add(); - resourceGraphBuilder.Add(); - resourceGraphBuilder.Add(); - resourceGraphBuilder.Add(); - - return resourceGraphBuilder.Build(); - } - - private static void BuildOneToOneRelationships(ResourceGraphBuilder resourceGraphBuilder) - { - resourceGraphBuilder.Add("oneToOnePrincipals"); - resourceGraphBuilder.Add("oneToOneDependents"); - resourceGraphBuilder.Add("oneToOneRequiredDependents"); - } - - private static void BuildOneToManyRelationships(ResourceGraphBuilder resourceGraphBuilder) - { - resourceGraphBuilder.Add("oneToManyPrincipals"); - resourceGraphBuilder.Add("oneToManyDependents"); - resourceGraphBuilder.Add("oneToMany-requiredDependents"); - } - - private static void BuildCollectiveRelationships(ResourceGraphBuilder resourceGraphBuilder) - { - resourceGraphBuilder.Add("multiPrincipals"); - resourceGraphBuilder.Add("multiDependents"); - } - } -} diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs deleted file mode 100644 index 11c2418ffa..0000000000 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Queries.Internal; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Objects; -using Moq; - -namespace UnitTests.Serialization -{ - public class SerializerTestsSetup : SerializationTestsSetupBase - { - private static readonly IncludeChainConverter IncludeChainConverter = new(); - - protected readonly TopLevelLinks DummyTopLevelLinks; - protected readonly ResourceLinks DummyResourceLinks; - protected readonly RelationshipLinks DummyRelationshipLinks; - - protected SerializerTestsSetup() - { - DummyTopLevelLinks = new TopLevelLinks - { - Self = "http://www.dummy.com/dummy-self-link", - Next = "http://www.dummy.com/dummy-next-link", - Prev = "http://www.dummy.com/dummy-prev-link", - First = "http://www.dummy.com/dummy-first-link", - Last = "http://www.dummy.com/dummy-last-link" - }; - - DummyResourceLinks = new ResourceLinks - { - Self = "http://www.dummy.com/dummy-resource-self-link" - }; - - DummyRelationshipLinks = new RelationshipLinks - { - Related = "http://www.dummy.com/dummy-relationship-related-link", - Self = "http://www.dummy.com/dummy-relationship-self-link" - }; - } - - protected ResponseSerializer GetResponseSerializer(IEnumerable> inclusionChains = null, - Dictionary metaDict = null, TopLevelLinks topLinks = null, ResourceLinks resourceLinks = null, - RelationshipLinks relationshipLinks = null) - where T : class, IIdentifiable - { - IEnumerable[] inclusionChainArray = inclusionChains?.ToArray(); - - IMetaBuilder meta = GetMetaBuilder(metaDict); - ILinkBuilder link = GetLinkBuilder(topLinks, resourceLinks, relationshipLinks); - IEnumerable includeConstraints = GetIncludeConstraints(inclusionChainArray); - IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(inclusionChainArray != null); - IFieldsToSerialize fieldsToSerialize = GetSerializableFields(); - IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor(); - IEvaluatedIncludeCache evaluatedIncludeCache = GetEvaluatedIncludeCache(inclusionChainArray); - var sparseFieldSetCache = new SparseFieldSetCache(Enumerable.Empty(), resourceDefinitionAccessor); - var options = new JsonApiOptions(); - - var resourceObjectBuilder = new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, resourceDefinitionAccessor, - options, evaluatedIncludeCache, sparseFieldSetCache); - - var request = new JsonApiRequest(); - - return new ResponseSerializer(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, resourceDefinitionAccessor, - sparseFieldSetCache, options, request); - } - - protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(IEnumerable> inclusionChains = null, - ResourceLinks resourceLinks = null, RelationshipLinks relationshipLinks = null) - { - IEnumerable[] inclusionChainArray = inclusionChains?.ToArray(); - - ILinkBuilder link = GetLinkBuilder(null, resourceLinks, relationshipLinks); - IEnumerable includeConstraints = GetIncludeConstraints(inclusionChainArray); - IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(inclusionChains != null); - IEvaluatedIncludeCache evaluatedIncludeCache = GetEvaluatedIncludeCache(inclusionChainArray); - IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor(); - var sparseFieldSetCache = new SparseFieldSetCache(Enumerable.Empty(), resourceDefinitionAccessor); - var options = new JsonApiOptions(); - - return new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, resourceDefinitionAccessor, options, - evaluatedIncludeCache, sparseFieldSetCache); - } - - private IIncludedResourceObjectBuilder GetIncludedBuilder(bool hasIncludeQueryString) - { - IFieldsToSerialize fieldsToSerialize = GetSerializableFields(); - ILinkBuilder linkBuilder = GetLinkBuilder(); - IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor(); - IRequestQueryStringAccessor queryStringAccessor = new FakeRequestQueryStringAccessor(hasIncludeQueryString ? "include=" : null); - var sparseFieldSetCache = new SparseFieldSetCache(Enumerable.Empty(), resourceDefinitionAccessor); - var options = new JsonApiOptions(); - - return new IncludedResourceObjectBuilder(fieldsToSerialize, linkBuilder, ResourceGraph, Enumerable.Empty(), - resourceDefinitionAccessor, queryStringAccessor, options, sparseFieldSetCache); - } - - private IResourceDefinitionAccessor GetResourceDefinitionAccessor() - { - var mock = new Mock(); - return mock.Object; - } - - private IMetaBuilder GetMetaBuilder(Dictionary meta = null) - { - var mock = new Mock(); - mock.Setup(metaBuilder => metaBuilder.Build()).Returns(meta); - return mock.Object; - } - - protected ILinkBuilder GetLinkBuilder(TopLevelLinks top = null, ResourceLinks resource = null, RelationshipLinks relationship = null) - { - var mock = new Mock(); - mock.Setup(linkBuilder => linkBuilder.GetTopLevelLinks()).Returns(top); - mock.Setup(linkBuilder => linkBuilder.GetResourceLinks(It.IsAny(), It.IsAny())).Returns(resource); - mock.Setup(linkBuilder => linkBuilder.GetRelationshipLinks(It.IsAny(), It.IsAny())).Returns(relationship); - return mock.Object; - } - - protected IFieldsToSerialize GetSerializableFields() - { - var mock = new Mock(); - mock.Setup(fields => fields.GetAttributes(It.IsAny())).Returns(type => ResourceGraph.GetResourceContext(type).Attributes); - mock.Setup(fields => fields.GetRelationships(It.IsAny())).Returns(type => ResourceGraph.GetResourceContext(type).Relationships); - return mock.Object; - } - - private IEnumerable GetIncludeConstraints(IEnumerable> inclusionChains = null) - { - var expressionsInScope = new List(); - - if (inclusionChains != null) - { - List chains = inclusionChains.Select(relationships => - new ResourceFieldChainExpression(relationships.Cast().ToImmutableArray())).ToList(); - - IncludeExpression includeExpression = IncludeChainConverter.FromRelationshipChains(chains); - expressionsInScope.Add(new ExpressionInScope(null, includeExpression)); - } - - var mock = new Mock(); - mock.Setup(provider => provider.GetConstraints()).Returns(expressionsInScope); - - IQueryConstraintProvider includeConstraintProvider = mock.Object; - return includeConstraintProvider.AsEnumerable(); - } - - private IEvaluatedIncludeCache GetEvaluatedIncludeCache(IEnumerable> inclusionChains = null) - { - if (inclusionChains == null) - { - return new EvaluatedIncludeCache(); - } - - List chains = inclusionChains.Select(relationships => - new ResourceFieldChainExpression(relationships.Cast().ToImmutableArray())).ToList(); - - IncludeExpression includeExpression = IncludeChainConverter.FromRelationshipChains(chains); - - var evaluatedIncludeCache = new EvaluatedIncludeCache(); - evaluatedIncludeCache.Set(includeExpression); - return evaluatedIncludeCache; - } - - /// - /// Minimal implementation of abstract JsonApiSerializer base class, with the purpose of testing the business logic for building the document structure. - /// - protected sealed class TestSerializer : BaseSerializer - { - public TestSerializer(IResourceObjectBuilder resourceObjectBuilder) - : base(resourceObjectBuilder) - { - } - - public Document PublicBuild(IIdentifiable resource, IReadOnlyCollection attributes = null, - IReadOnlyCollection relationships = null) - { - return Build(resource, attributes, relationships); - } - - public Document PublicBuild(IReadOnlyCollection resources, IReadOnlyCollection attributes = null, - IReadOnlyCollection relationships = null) - { - return Build(resources, attributes, relationships); - } - } - } -} diff --git a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs deleted file mode 100644 index d3946f15a0..0000000000 --- a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Internal; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Objects; -using Moq; -using UnitTests.TestModels; -using Xunit; - -namespace UnitTests.Serialization.Server -{ - public sealed class IncludedResourceObjectBuilderTests : SerializerTestsSetup - { - [Fact] - public void BuildIncluded_DeeplyNestedCircularChainOfSingleData_CanBuild() - { - // Arrange - (Article article, Person author, _, Person reviewer, _) = GetAuthorChainInstances(); - IReadOnlyCollection authorChain = GetIncludedRelationshipsChain("author.blogs.reviewer.favoriteFood"); - IncludedResourceObjectBuilder builder = GetBuilder(); - - // Act - builder.IncludeRelationshipChain(authorChain, article); - IList result = builder.Build(); - - // Assert - Assert.Equal(6, result.Count); - - ResourceObject authorResourceObject = result.Single(ro => ro.Type == "people" && ro.Id == author.StringId); - 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"].Data.SingleValue; - Assert.Equal(reviewer.FavoriteFood.StringId, reviewerFoodRelation.Id); - } - - [Fact] - public void BuildIncluded_DeeplyNestedCircularChainOfManyData_BuildsWithoutDuplicates() - { - // Arrange - (Article article, Person author, _, _, _) = GetAuthorChainInstances(); - Article secondArticle = ArticleFaker.Generate(); - secondArticle.Author = author; - IncludedResourceObjectBuilder builder = GetBuilder(); - - // Act - IReadOnlyCollection authorChain = GetIncludedRelationshipsChain("author.blogs.reviewer.favoriteFood"); - builder.IncludeRelationshipChain(authorChain, article); - builder.IncludeRelationshipChain(authorChain, secondArticle); - - // Assert - IList result = builder.Build(); - Assert.Equal(6, result.Count); - } - - [Fact] - public void BuildIncluded_OverlappingDeeplyNestedCircularChains_CanBuild() - { - // Arrange - IReadOnlyCollection authorChain = GetIncludedRelationshipsChain("author.blogs.reviewer.favoriteFood"); - (Article article, Person author, _, Person reviewer, Food reviewerFood) = GetAuthorChainInstances(); - Blog sharedBlog = author.Blogs.First(); - Person sharedBlogAuthor = reviewer; - Song authorSong = GetReviewerChainInstances(article, sharedBlog, sharedBlogAuthor); - IReadOnlyCollection reviewerChain = GetIncludedRelationshipsChain("reviewer.blogs.author.favoriteSong"); - IncludedResourceObjectBuilder builder = GetBuilder(); - - // Act - builder.IncludeRelationshipChain(authorChain, article); - builder.IncludeRelationshipChain(reviewerChain, article); - IList result = builder.Build(); - - // Assert - Assert.Equal(10, result.Count); - ResourceObject overlappingBlogResourceObject = result.Single(ro => ro.Type == "blogs" && ro.Id == sharedBlog.StringId); - - Assert.Equal(2, overlappingBlogResourceObject.Relationships.Keys.Count); - List nonOverlappingBlogs = result.Where(ro => ro.Type == "blogs" && ro.Id != sharedBlog.StringId).ToList(); - - foreach (ResourceObject blog in nonOverlappingBlogs) - { - Assert.Single(blog.Relationships.Keys); - } - - Assert.Equal(authorSong.StringId, sharedBlogAuthor.FavoriteSong.StringId); - Assert.Equal(reviewerFood.StringId, sharedBlogAuthor.FavoriteFood.StringId); - } - - [Fact] - public void BuildIncluded_DuplicateChildrenMultipleChains_OnceInOutput() - { - Person person = PersonFaker.Generate(); - List
articles = ArticleFaker.Generate(5); - articles.ForEach(article => article.Author = person); - articles.ForEach(article => article.Reviewer = person); - IncludedResourceObjectBuilder builder = GetBuilder(); - IReadOnlyCollection authorChain = GetIncludedRelationshipsChain("author"); - IReadOnlyCollection reviewerChain = GetIncludedRelationshipsChain("reviewer"); - - foreach (Article article in articles) - { - builder.IncludeRelationshipChain(authorChain, article); - builder.IncludeRelationshipChain(reviewerChain, article); - } - - IList result = builder.Build(); - Assert.Single(result); - Assert.Equal(person.Name, result[0].Attributes["name"]); - Assert.Equal(person.StringId, result[0].Id); - } - - private Song GetReviewerChainInstances(Article article, Blog sharedBlog, Person sharedBlogAuthor) - { - Person reviewer = PersonFaker.Generate(); - article.Reviewer = reviewer; - - List blogs = BlogFaker.Generate(1); - blogs.Add(sharedBlog); - reviewer.Blogs = blogs.ToHashSet(); - - blogs[0].Author = reviewer; - Person author = PersonFaker.Generate(); - blogs[1].Author = sharedBlogAuthor; - - Song authorSong = SongFaker.Generate(); - author.FavoriteSong = authorSong; - sharedBlogAuthor.FavoriteSong = authorSong; - - Song reviewerSong = SongFaker.Generate(); - reviewer.FavoriteSong = reviewerSong; - - return authorSong; - } - - private AuthorChainInstances GetAuthorChainInstances() - { - Article article = ArticleFaker.Generate(); - Person author = PersonFaker.Generate(); - article.Author = author; - - List blogs = BlogFaker.Generate(2); - author.Blogs = blogs.ToHashSet(); - - blogs[0].Reviewer = author; - Person reviewer = PersonFaker.Generate(); - blogs[1].Reviewer = reviewer; - - Food authorFood = FoodFaker.Generate(); - author.FavoriteFood = authorFood; - Food reviewerFood = FoodFaker.Generate(); - reviewer.FavoriteFood = reviewerFood; - - return new AuthorChainInstances(article, author, authorFood, reviewer, reviewerFood); - } - - private IReadOnlyCollection GetIncludedRelationshipsChain(string chain) - { - var parsedChain = new List(); - ResourceContext resourceContext = ResourceGraph.GetResourceContext
(); - string[] splitPath = chain.Split('.'); - - foreach (string requestedRelationship in splitPath) - { - RelationshipAttribute relationship = resourceContext.GetRelationshipByPublicName(requestedRelationship); - - parsedChain.Add(relationship); - resourceContext = ResourceGraph.GetResourceContext(relationship.RightType); - } - - return parsedChain; - } - - private IncludedResourceObjectBuilder GetBuilder() - { - IFieldsToSerialize fields = GetSerializableFields(); - ILinkBuilder links = GetLinkBuilder(); - IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock().Object; - var queryStringAccessor = new FakeRequestQueryStringAccessor(); - var options = new JsonApiOptions(); - IEnumerable constraintProviders = Array.Empty(); - var sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); - - return new IncludedResourceObjectBuilder(fields, links, ResourceGraph, constraintProviders, resourceDefinitionAccessor, queryStringAccessor, - options, sparseFieldSetCache); - } - - private sealed class AuthorChainInstances - { - public Article Article { get; } - public Person Author { get; } - public Food AuthorFood { get; } - public Person Reviewer { get; } - public Food ReviewerFood { get; } - - public AuthorChainInstances(Article article, Person author, Food authorFood, Person reviewer, Food reviewerFood) - { - Article = article; - Author = author; - AuthorFood = authorFood; - Reviewer = reviewer; - ReviewerFood = reviewerFood; - } - - public void Deconstruct(out Article article, out Person author, out Food authorFood, out Person reviewer, out Food reviewerFood) - { - article = Article; - author = Author; - authorFood = AuthorFood; - reviewer = Reviewer; - reviewerFood = ReviewerFood; - } - } - } -} diff --git a/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs deleted file mode 100644 index e103cac808..0000000000 --- a/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Objects; -using UnitTests.TestModels; -using Xunit; - -namespace UnitTests.Serialization.Server -{ - public sealed class ResponseResourceObjectBuilderTests : SerializerTestsSetup - { - private const string RelationshipName = "dependents"; - private readonly List _relationshipsForBuild; - - public ResponseResourceObjectBuilderTests() - { - _relationshipsForBuild = ResourceGraph.GetRelationships(relationship => new - { - relationship.Dependents - }).ToList(); - } - - [Fact] - public void Build_RelationshipNotIncludedAndLinksEnabled_RelationshipObjectWithLinks() - { - // Arrange - var resource = new OneToManyPrincipal - { - Id = 10 - }; - - ResponseResourceObjectBuilder builder = GetResponseResourceObjectBuilder(relationshipLinks: DummyRelationshipLinks); - - // Act - ResourceObject resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); - - // Assert - 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] - public void Build_RelationshipNotIncludedAndLinksDisabled_NoRelationshipObject() - { - // Arrange - var resource = new OneToManyPrincipal - { - Id = 10 - }; - - ResponseResourceObjectBuilder builder = GetResponseResourceObjectBuilder(); - - // Act - ResourceObject resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); - - // Assert - Assert.Null(resourceObject.Relationships); - } - - [Fact] - public void Build_RelationshipIncludedAndLinksDisabled_RelationshipObjectWithData() - { - // Arrange - var resource = new OneToManyPrincipal - { - Id = 10, - Dependents = new HashSet - { - new() - { - Id = 20 - } - } - }; - - ResponseResourceObjectBuilder builder = GetResponseResourceObjectBuilder(_relationshipsForBuild.AsEnumerable()); - - // Act - ResourceObject resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); - - // Assert - 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_RelationshipObjectWithDataAndLinks() - { - // Arrange - var resource = new OneToManyPrincipal - { - Id = 10, - Dependents = new HashSet - { - new() - { - Id = 20 - } - } - }; - - ResponseResourceObjectBuilder builder = - GetResponseResourceObjectBuilder(_relationshipsForBuild.AsEnumerable(), relationshipLinks: DummyRelationshipLinks); - - // Act - ResourceObject resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); - - // Assert - 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 deleted file mode 100644 index 6532b2085e..0000000000 --- a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs +++ /dev/null @@ -1,450 +0,0 @@ -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 UnitTests.TestModels; -using Xunit; - -namespace UnitTests.Serialization.Server -{ - public sealed class ResponseSerializerTests : SerializerTestsSetup - { - [Fact] - public void SerializeSingle_ResourceWithDefaultTargetFields_CanSerialize() - { - // Arrange - var resource = new TestResource - { - Id = 1, - StringField = "value", - NullableIntField = 123 - }; - - ResponseSerializer serializer = GetResponseSerializer(); - - // Act - string serialized = serializer.SerializeSingle(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 SerializeMany_ResourceWithDefaultTargetFields_CanSerialize() - { - // Arrange - var resource = new TestResource - { - Id = 1, - StringField = "value", - NullableIntField = 123 - }; - - ResponseSerializer serializer = GetResponseSerializer(); - - // Act - string serialized = serializer.SerializeMany(resource.AsArray()); - - // 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_ResourceWithIncludedRelationships_CanSerialize() - { - // Arrange - var resource = new MultipleRelationshipsPrincipalPart - { - Id = 1, - PopulatedToOne = new OneToOneDependent - { - Id = 10 - }, - PopulatedToManies = new HashSet - { - new() - { - Id = 20 - } - } - }; - - ResourceContext resourceContext = ResourceGraph.GetResourceContext(); - List> chain = resourceContext.Relationships.Select(relationship => relationship.AsEnumerable()).ToList(); - - ResponseSerializer serializer = GetResponseSerializer(chain); - - // Act - string serialized = serializer.SerializeSingle(resource); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""multiPrincipals"", - ""id"":""1"", - ""attributes"":{ ""attributeMember"":null }, - ""relationships"":{ - ""populatedToOne"":{ - ""data"":{ - ""type"":""oneToOneDependents"", - ""id"":""10"" - } - }, - ""emptyToOne"": { ""data"":null }, - ""populatedToManies"":{ - ""data"":[ - { - ""type"":""oneToManyDependents"", - ""id"":""20"" - } - ] - }, - ""emptyToManies"": { ""data"":[ ] }, - ""multi"":{ ""data"":null } - } - }, - ""included"":[ - { - ""type"":""oneToOneDependents"", - ""id"":""10"", - ""attributes"":{ ""attributeMember"":null } - }, - { - ""type"":""oneToManyDependents"", - ""id"":""20"", - ""attributes"":{ ""attributeMember"":null } - } - ] - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_ResourceWithDeeplyIncludedRelationships_CanSerialize() - { - // Arrange - var deeplyIncludedResource = new OneToManyPrincipal - { - Id = 30, - AttributeMember = "deep" - }; - - var includedResource = new OneToManyDependent - { - Id = 20, - Principal = deeplyIncludedResource - }; - - var resource = new MultipleRelationshipsPrincipalPart - { - Id = 10, - PopulatedToManies = new HashSet - { - includedResource - } - }; - - ResourceContext outerResourceContext = ResourceGraph.GetResourceContext(); - - List> chains = outerResourceContext.Relationships.Select(relationship => - { - List chain = relationship.AsList(); - - if (relationship.PublicName != "populatedToManies") - { - return chain; - } - - ResourceContext innerResourceContext = ResourceGraph.GetResourceContext(); - chain.AddRange(innerResourceContext.Relationships); - return chain; - }).ToList(); - - ResponseSerializer serializer = GetResponseSerializer(chains); - - // Act - string serialized = serializer.SerializeSingle(resource); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""multiPrincipals"", - ""id"":""10"", - ""attributes"":{ - ""attributeMember"":null - }, - ""relationships"":{ - ""populatedToOne"":{ - ""data"":null - }, - ""emptyToOne"":{ - ""data"":null - }, - ""populatedToManies"":{ - ""data"":[ - { - ""type"":""oneToManyDependents"", - ""id"":""20"" - } - ] - }, - ""emptyToManies"":{ - ""data"":[] - }, - ""multi"":{ - ""data"":null - } - } - }, - ""included"":[ - { - ""type"":""oneToManyDependents"", - ""id"":""20"", - ""attributes"":{ - ""attributeMember"":null - }, - ""relationships"":{ - ""principal"":{ - ""data"":{ - ""type"":""oneToManyPrincipals"", - ""id"":""30"" - } - } - } - }, - { - ""type"":""oneToManyPrincipals"", - ""id"":""30"", - ""attributes"":{ - ""attributeMember"":""deep"" - } - } - ] - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_Null_CanSerialize() - { - // Arrange - ResponseSerializer serializer = GetResponseSerializer(); - - // Act - string serialized = serializer.SerializeSingle(null); - - // Assert - const string expectedFormatted = @"{ ""data"": null }"; - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeList_EmptyList_CanSerialize() - { - // Arrange - ResponseSerializer serializer = GetResponseSerializer(); - - // Act - string serialized = serializer.SerializeMany(new List()); - - // Assert - const string expectedFormatted = @"{ ""data"": [] }"; - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_ResourceWithLinksEnabled_CanSerialize() - { - // Arrange - var resource = new OneToManyPrincipal - { - Id = 10 - }; - - ResponseSerializer serializer = GetResponseSerializer(topLinks: DummyTopLevelLinks, - relationshipLinks: DummyRelationshipLinks, resourceLinks: DummyResourceLinks); - - // Act - string serialized = serializer.SerializeSingle(resource); - - // Assert - const string expectedFormatted = @"{ - ""links"":{ - ""self"":""http://www.dummy.com/dummy-self-link"", - ""first"":""http://www.dummy.com/dummy-first-link"", - ""last"":""http://www.dummy.com/dummy-last-link"", - ""prev"":""http://www.dummy.com/dummy-prev-link"", - ""next"":""http://www.dummy.com/dummy-next-link"" - }, - ""data"":{ - ""type"":""oneToManyPrincipals"", - ""id"":""10"", - ""attributes"":{ - ""attributeMember"":null - }, - ""relationships"":{ - ""dependents"":{ - ""links"":{ - ""self"":""http://www.dummy.com/dummy-relationship-self-link"", - ""related"":""http://www.dummy.com/dummy-relationship-related-link"" - } - } - }, - ""links"":{ - ""self"":""http://www.dummy.com/dummy-resource-self-link"" - } - } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_ResourceWithMeta_IncludesMetaInResult() - { - // Arrange - var meta = new Dictionary - { - ["test"] = "meta" - }; - - var resource = new OneToManyPrincipal - { - Id = 10 - }; - - ResponseSerializer serializer = GetResponseSerializer(metaDict: meta); - - // Act - string serialized = serializer.SerializeSingle(resource); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""oneToManyPrincipals"", - ""id"":""10"", - ""attributes"":{ - ""attributeMember"":null - } - }, - ""meta"":{ ""test"": ""meta"" } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_NullWithLinksAndMeta_StillShowsLinksAndMeta() - { - // Arrange - var meta = new Dictionary - { - ["test"] = "meta" - }; - - ResponseSerializer serializer = GetResponseSerializer(metaDict: meta, topLinks: DummyTopLevelLinks, - relationshipLinks: DummyRelationshipLinks, resourceLinks: DummyResourceLinks); - - // Act - string serialized = serializer.SerializeSingle(null); - - // Assert - const string expectedFormatted = @"{ - ""links"":{ - ""self"":""http://www.dummy.com/dummy-self-link"", - ""first"":""http://www.dummy.com/dummy-first-link"", - ""last"":""http://www.dummy.com/dummy-last-link"", - ""prev"":""http://www.dummy.com/dummy-prev-link"", - ""next"":""http://www.dummy.com/dummy-next-link"" - }, - ""data"": null, - ""meta"":{ ""test"": ""meta"" } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeError_Error_CanSerialize() - { - // Arrange - var error = new ErrorObject(HttpStatusCode.InsufficientStorage) - { - Title = "title", - Detail = "detail" - }; - - string expectedJson = JsonSerializer.Serialize(new - { - errors = new[] - { - new - { - id = error.Id, - status = "507", - title = "title", - detail = "detail" - } - } - }); - - ResponseSerializer serializer = GetResponseSerializer(); - - // Act - string result = serializer.Serialize(error); - - // Assert - Assert.Equal(expectedJson, result); - } - } -} diff --git a/wiki/v4/content/serialization.md b/wiki/v4/content/serialization.md deleted file mode 100644 index 7145333b52..0000000000 --- a/wiki/v4/content/serialization.md +++ /dev/null @@ -1,111 +0,0 @@ - -# Serialization - -The main change for serialization is that we have split the serialization responsibilities into two parts: - -* **Response (de)serializers** - (de)Serialization regarding serving or interpreting a response. -* **Request (de)serializer** - (de)Serialization regarding creating or interpreting a request. - -This split is done because during deserialization, some parts are relevant only for *client*-side parsing whereas others are only for *server*-side parsing. for example, a server deserializer will never have to deal with a `included` object list. Similarly, in serialization, a client serializer will for example never ever have to populate any other top-level members than the primary data (like `meta`, `included`). - -Throughout the document and the code when referring to fields, members, object types, the technical language of JSON:API spec is used. At the core of (de)serialization is the -`Document` class, [see document spec](https://jsonapi.org/format/#document-structure). - -## Changes - -In this section we will detail the changes made to the (de)serialization compared to the previous version. - -### Deserialization - -The previous `JsonApiDeSerializer` implementation is now split into a `RequestDeserializer` and `ResponseDeserializer`. Both inherit from `BaseDocumentParser` which does the shared parsing. - -#### BaseDocumentParser - -This (base) class is responsible for: - -* Converting the serialized string content into an intance of the `Document` class. Which is the most basic version of JSON:API which has a `Data`, `Meta` and `Included` property. -* Building instances of the corresponding resource class (eg `Article`) by going through the document's primary data (`Document.Data`) For the spec for this: [Document spec](https://jsonapi.org/format/#document-top-level). - -Responsibility of any implementation the base class-specific parsing is shifted through the abstract `BaseDocumentParser.AfterProcessField()` method. This method is fired once each time after a `AttrAttribute` or `RelationshipAttribute` is processed. It allows a implementation of `BaseDocumentParser` to intercept the parsing and add steps that are only required for new implementations. - -#### ResponseDeserializer - -The client deserializer complements the base deserialization by - -* overriding the `AfterProcessField` method which takes care of the Included section \* after a relationship was deserialized, it finds the appended included object and adds it attributs and (nested) relationships -* taking care of remaining top-level members. that are only relevant to a client-side parser (meta data, server-side errors, links). - -#### RequestDeserializer - -For server-side parsing, no extra parsing needs to be done after the base deserialization is completed. It only needs to keep track of which `AttrAttribute`s and `RelationshipAttribute`s were targeted by a request. This is needed for the internals of JADNC (eg the repository layer). - -* The `AfterProcessField` method is overriden so that every attribute and relationship is registered with the `ITargetedFields` service after it is processed. - -## Serialization - -Like with the deserializers, `JsonApiSerializer` is now split up into these classes (indentation implies hierarchy/extending): - -* `IncludedResourceObjectBuilder` - -* `ResourceObjectBuilder` - *abstract* - * `DocumentBuilder` - *abstract* - - * `ResponseSerializer` - * `RequestSerializer` - -### ResourceObjectBuilder - -At the core of serialization is the `ResourceObject` class [see resource object spec](https://jsonapi.org/format/#document-resource-objects). - -ResourceObjectBuilder is responsible for Building a `ResourceObject` from an entity given a list of `AttrAttribute`s and `RelationshipAttribute`s. - Note: the resource object builder is NOT responsible for figuring out which attributes and relationships should be included in the serialization result, because this differs depending on an the implementation being client or server side. Instead, it is provided with the list. - -Additionally, client and server serializers also differ in how relationship members ([see relationship member spec](https://jsonapi.org/format/#document-resource-object-attributes) are formatted. The responsibility for handling this is again shifted, this time by virtual `ResourceObjectBuilder.GetRelationshipData()` method. This method is fired once each time a `RelationshipAttribute` is processed, allowing for additional serialization (like adding links or metadata). - -This time, the `GetRelationshipData()` method is not abstract, but virtual with a default implementation. This default implementation is to just create a `RelationshipData` with primary data (like `{"related-foo": { "data": { "id": 1" "type": "foobar"}}}`). Some implementations (server, included builder) need additional logic, others don't (client). - -### BaseDocumentBuilder -Responsible for - -- Calling the base resource object serialization for one (or many) entities and wrapping the result in a `Document`. - -Thats all. It does not figure out which attributes or relationships are to be serialized. - -### RequestSerializer - -Responsible for figuring out which attributes and relationships need to be serialized and calling the base document builder with that. -For example: - -- for a POST request, this is often (almost) all attributes. -- for a PATCH request, this is usually a small subset of attributes. - -Note that the client serializer is relatively skinny, because no top-level data (included, meta, links) will ever have to be added anywhere in the document. - -### ResponseSerializer - -Responsible for figuring out which attributes and relationships need to be serialized and calling the base document builder with that. -For example, for a GET request, all attributes are usually included in the output, unless - -* Sparse field selection was applied in the client request -* Runtime attribute hiding was applied, see [JADNC docs](https://json-api-dotnet.github.io/JsonApiDotNetCore/usage/resources/resource-definitions.html#runtime-attribute-filtering) - -The server serializer is also responsible for adding top-level meta data and links and appending included relationships. For this the `GetRelationshipData()` is overriden: - -* it adds links to the `RelationshipData` object (if configured to do so, see `ILinksConfiguration`). -* it checks if the processed relationship needs to be enclosed in the `included` list. If so, it calls the `IIncludedResourceObjectBuilder` to take care of that. - -### IncludedResourceObjectBuilder -Responsible for building the *included member* of a `Document`. Note that `IncludedResourceObjectBuilder` extends `ResourceObjectBuilder` and not `BaseDocumentBuilder` because it does not need to build an entire document but only resource objects. - -Responsible for building the _included member_ of a `Document`. Note that `IncludedResourceObjectBuilder` extends `ResourceObjectBuilder` and not `DocumentBuilder` because it does not need to build an entire document but only resource objects. - -Relationship _inclusion chains_ are at the core of building the included member. For example, consider the request `articles?included=author.blogs.reviewers.favorite-food,reviewer.blogs.author.favorite-song`. It contains the following (complex) inclusion chains: - -1. `author.blogs.reviewers.favorite-food` -2. `reviewer.blogs.author.favorite-song` - -Like with the `RequestSerializer` and `ResponseSerializer`, the `IncludedResourceObjectBuilder` is responsible for calling the base resource object builder with the list of attributes and relationships. For this implementation, these lists depend strongly on the inclusion chains. The above complex example demonstrates this (note: in this example the relationships `author` and `reviewer` are of the same resource `people`): - -* people that were included as reviewers from inclusion chain (1) should come with their `favorite-food` included, but not those from chain (2) -* people that were included as authors from inclusion chain (2) should come with their `favorite-song` included, but not those from chain (1). -* a person that was included as both an reviewer and author (i.e. targeted by both chain (1) and (2)), both `favorite-food` and `favorite-song` need to be present. - -To achieve this all of this, the `IncludedResourceObjectBuilder` needs to recursively parse an inclusion chain and make sure it does not append the same included more than once. This strategy is different from that of the ResponseSerializer, and for that reason it is a separate service. From 05f155ea1c3543aaeb661c6e3c009e70370a2a41 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Oct 2021 15:14:36 +0200 Subject: [PATCH 33/49] Fixed: crash in test serializer on assertion failure --- test/TestBuildingBlocks/HttpResponseMessageExtensions.cs | 2 +- test/TestBuildingBlocks/IntegrationTest.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs b/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs index 873cec6d3f..4803600dec 100644 --- a/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs +++ b/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs @@ -30,7 +30,7 @@ public AndConstraint HaveStatusCode(HttpStatusCod if (Subject.StatusCode != statusCode) { string responseText = Subject.Content.ReadAsStringAsync().Result; - Subject.StatusCode.Should().Be(statusCode, $"response body returned was:\n{responseText}"); + Subject.StatusCode.Should().Be(statusCode, string.IsNullOrEmpty(responseText) ? null : $"response body returned was:\n{responseText}"); } return new AndConstraint(this); diff --git a/test/TestBuildingBlocks/IntegrationTest.cs b/test/TestBuildingBlocks/IntegrationTest.cs index 1d4eeb8923..61601c0f6f 100644 --- a/test/TestBuildingBlocks/IntegrationTest.cs +++ b/test/TestBuildingBlocks/IntegrationTest.cs @@ -95,6 +95,11 @@ private TResponseDocument DeserializeResponse(string response return (TResponseDocument)(object)responseText; } + if (string.IsNullOrEmpty(responseText)) + { + return default; + } + try { return JsonSerializer.Deserialize(responseText, SerializerOptions); From fbdcefb2984db8c0da9d4f5960b3fff062c483c0 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Oct 2021 15:24:08 +0200 Subject: [PATCH 34/49] Removed RequestScopedServiceProvider --- .../IRequestScopedServiceProvider.cs | 12 ------- .../JsonApiApplicationBuilder.cs | 1 - .../JsonApiModelMetadataProvider.cs | 9 ++--- .../Configuration/JsonApiValidationFilter.cs | 30 +++++++++++----- .../RequestScopedServiceProvider.cs | 34 ------------------ .../ServiceCollectionExtensionsTests.cs | 6 ---- .../RequestScopedServiceProviderTests.cs | 35 ------------------- test/UnitTests/TestScopedServiceProvider.cs | 28 --------------- 8 files changed, 26 insertions(+), 129 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Configuration/IRequestScopedServiceProvider.cs delete mode 100644 src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs delete mode 100644 test/UnitTests/Internal/RequestScopedServiceProviderTests.cs delete mode 100644 test/UnitTests/TestScopedServiceProvider.cs diff --git a/src/JsonApiDotNetCore/Configuration/IRequestScopedServiceProvider.cs b/src/JsonApiDotNetCore/Configuration/IRequestScopedServiceProvider.cs deleted file mode 100644 index 327eeca353..0000000000 --- a/src/JsonApiDotNetCore/Configuration/IRequestScopedServiceProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Configuration -{ - /// - /// An interface used to separate the registration of the global from a request-scoped service provider. This is useful - /// in cases when we need to manually resolve services from the request scope (e.g. operation processors). - /// - public interface IRequestScopedServiceProvider : IServiceProvider - { - } -} diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index fc428df3d1..0547af7aba 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -175,7 +175,6 @@ private void AddMiddlewareLayer() _services.AddSingleton(); _services.AddSingleton(sp => sp.GetRequiredService()); _services.AddSingleton(); - _services.AddSingleton(); _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs b/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs index 19f9edc531..07f15db8a6 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; @@ -13,18 +14,18 @@ internal sealed class JsonApiModelMetadataProvider : DefaultModelMetadataProvide private readonly JsonApiValidationFilter _jsonApiValidationFilter; /// - public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider, IRequestScopedServiceProvider serviceProvider) + public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider, IHttpContextAccessor httpContextAccessor) : base(detailsProvider) { - _jsonApiValidationFilter = new JsonApiValidationFilter(serviceProvider); + _jsonApiValidationFilter = new JsonApiValidationFilter(httpContextAccessor); } /// public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider, IOptions optionsAccessor, - IRequestScopedServiceProvider serviceProvider) + IHttpContextAccessor httpContextAccessor) : base(detailsProvider, optionsAccessor) { - _jsonApiValidationFilter = new JsonApiValidationFilter(serviceProvider); + _jsonApiValidationFilter = new JsonApiValidationFilter(httpContextAccessor); } /// diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs index 7cefd4e120..c78a2936dc 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs @@ -13,19 +13,21 @@ namespace JsonApiDotNetCore.Configuration /// internal sealed class JsonApiValidationFilter : IPropertyValidationFilter { - private readonly IRequestScopedServiceProvider _serviceProvider; + private readonly IHttpContextAccessor _httpContextAccessor; - public JsonApiValidationFilter(IRequestScopedServiceProvider serviceProvider) + public JsonApiValidationFilter(IHttpContextAccessor httpContextAccessor) { - ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); + ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); - _serviceProvider = serviceProvider; + _httpContextAccessor = httpContextAccessor; } /// public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry) { - var request = _serviceProvider.GetRequiredService(); + IServiceProvider serviceProvider = GetServiceProvider(); + + var request = serviceProvider.GetRequiredService(); if (IsId(entry.Key)) { @@ -39,17 +41,27 @@ public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEnt return false; } - var httpContextAccessor = _serviceProvider.GetRequiredService(); - - if (httpContextAccessor.HttpContext!.Request.Method == HttpMethods.Patch || request.WriteOperation == WriteOperationKind.UpdateResource) + if (_httpContextAccessor.HttpContext!.Request.Method == HttpMethods.Patch || request.WriteOperation == WriteOperationKind.UpdateResource) { - var targetedFields = _serviceProvider.GetRequiredService(); + var targetedFields = serviceProvider.GetRequiredService(); return IsFieldTargeted(entry, targetedFields); } return true; } + private IServiceProvider GetServiceProvider() + { + HttpContext httpContext = _httpContextAccessor.HttpContext; + + if (httpContext == null) + { + throw new InvalidOperationException("Cannot resolve scoped services outside the context of an HTTP request."); + } + + return httpContext.RequestServices; + } + private static bool IsId(string key) { return key == nameof(Identifiable.Id) || key.EndsWith($".{nameof(Identifiable.Id)}", StringComparison.Ordinal); diff --git a/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs b/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs deleted file mode 100644 index 649a219c0b..0000000000 --- a/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using Microsoft.AspNetCore.Http; - -namespace JsonApiDotNetCore.Configuration -{ - /// - public sealed class RequestScopedServiceProvider : IRequestScopedServiceProvider - { - private readonly IHttpContextAccessor _httpContextAccessor; - - public RequestScopedServiceProvider(IHttpContextAccessor httpContextAccessor) - { - ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); - - _httpContextAccessor = httpContextAccessor; - } - - /// - public object GetService(Type serviceType) - { - ArgumentGuard.NotNull(serviceType, nameof(serviceType)); - - if (_httpContextAccessor.HttpContext == null) - { - throw new InvalidOperationException($"Cannot resolve scoped service '{serviceType.FullName}' outside the context of an HTTP request. " + - "If you are hitting this error in automated tests, you should instead inject your own " + - "IRequestScopedServiceProvider implementation. See the GitHub repository for how we do this internally. " + - "https://github.com/json-api-dotnet/JsonApiDotNetCore/search?q=TestScopedServiceProvider&unscoped_q=TestScopedServiceProvider"); - } - - return _httpContextAccessor.HttpContext.RequestServices.GetService(serviceType); - } - } -} diff --git a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index 542abd496a..c5c41a8b63 100644 --- a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -29,10 +29,6 @@ public void RegisterResource_DeviatingDbContextPropertyName_RegistersCorrectly() services.AddLogging(); services.AddDbContext(options => options.UseInMemoryDatabase("UnitTestDb")); - // this is required because the DbContextResolver requires access to the current HttpContext - // to get the request scoped DbContext instance - services.AddScoped(); - // Act services.AddJsonApi(); @@ -171,8 +167,6 @@ public void AddJsonApi_With_Context_Uses_Resource_Type_Name_If_NoOtherSpecified( services.AddLogging(); services.AddDbContext(options => options.UseInMemoryDatabase(Guid.NewGuid().ToString())); - services.AddScoped(); - // Act services.AddJsonApi(); diff --git a/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs b/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs deleted file mode 100644 index 49313b4364..0000000000 --- a/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace UnitTests.Internal -{ - public sealed class RequestScopedServiceProviderTests - { - [Fact] - public void When_http_context_is_unavailable_it_must_fail() - { - // Arrange - Type serviceType = typeof(IIdentifiable); - - var provider = new RequestScopedServiceProvider(new HttpContextAccessor()); - - // Act - Action action = () => provider.GetRequiredService(serviceType); - - // Assert - var exception = Assert.Throws(action); - - Assert.StartsWith($"Cannot resolve scoped service '{serviceType.FullName}' outside the context of an HTTP request.", exception.Message); - } - - [UsedImplicitly(ImplicitUseTargetFlags.Itself)] - private sealed class Model : Identifiable - { - } - } -} diff --git a/test/UnitTests/TestScopedServiceProvider.cs b/test/UnitTests/TestScopedServiceProvider.cs deleted file mode 100644 index f11b031997..0000000000 --- a/test/UnitTests/TestScopedServiceProvider.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using JsonApiDotNetCore.Configuration; -using Microsoft.AspNetCore.Http; -using Moq; - -namespace UnitTests -{ - public sealed class TestScopedServiceProvider : IRequestScopedServiceProvider - { - private readonly IServiceProvider _serviceProvider; - private readonly Mock _httpContextAccessorMock = new(); - - public TestScopedServiceProvider(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public object GetService(Type serviceType) - { - if (serviceType == typeof(IHttpContextAccessor)) - { - return _httpContextAccessorMock.Object; - } - - return _serviceProvider.GetService(serviceType); - } - } -} From 7fcb58e135f31f4ef9943fef863cce0a21945dab Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Oct 2021 15:45:51 +0200 Subject: [PATCH 35/49] Use sets for include expressions --- .../SerializationBenchmarkBase.cs | 2 +- .../Expressions/IncludeChainConverter.cs | 8 ++--- .../Expressions/IncludeElementExpression.cs | 10 +++--- .../Queries/Expressions/IncludeExpression.cs | 10 +++--- .../Expressions/QueryExpressionRewriter.cs | 4 +-- .../Queries/Internal/QueryLayerComposer.cs | 34 +++++++++---------- .../Resources/IResourceDefinition.cs | 2 +- .../Resources/IResourceDefinitionAccessor.cs | 2 +- .../Resources/JsonApiResourceDefinition.cs | 2 +- .../Resources/ResourceDefinitionAccessor.cs | 2 +- .../Serialization/ResponseModelAdapter.cs | 8 ++--- .../Reading/MoonDefinition.cs | 2 +- .../Reading/PlanetDefinition.cs | 2 +- .../IncludeParseTests.cs | 2 +- .../ServiceCollectionExtensionsTests.cs | 4 +-- 15 files changed, 47 insertions(+), 47 deletions(-) diff --git a/benchmarks/Serialization/SerializationBenchmarkBase.cs b/benchmarks/Serialization/SerializationBenchmarkBase.cs index 3846720bbc..f7c578b0d5 100644 --- a/benchmarks/Serialization/SerializationBenchmarkBase.cs +++ b/benchmarks/Serialization/SerializationBenchmarkBase.cs @@ -130,7 +130,7 @@ public sealed class ResourceA : Identifiable private sealed class FakeResourceDefinitionAccessor : IResourceDefinitionAccessor { - public IImmutableList OnApplyIncludes(Type resourceType, IImmutableList existingIncludes) + public IImmutableSet OnApplyIncludes(Type resourceType, IImmutableSet existingIncludes) { return existingIncludes; } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs index dbe3b9b0dd..d1c097676b 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs @@ -68,11 +68,11 @@ public IncludeExpression FromRelationshipChains(IEnumerable elements = ConvertChainsToElements(chains); + IImmutableSet elements = ConvertChainsToElements(chains); return elements.Any() ? new IncludeExpression(elements) : IncludeExpression.Empty; } - private static IImmutableList ConvertChainsToElements(IEnumerable chains) + private static IImmutableSet ConvertChainsToElements(IEnumerable chains) { var rootNode = new MutableIncludeNode(null); @@ -81,7 +81,7 @@ private static IImmutableList ConvertChainsToElements( ConvertChainToElement(chain, rootNode); } - return rootNode.Children.Values.Select(child => child.ToExpression()).ToImmutableArray(); + return rootNode.Children.Values.Select(child => child.ToExpression()).ToImmutableHashSet(); } private static void ConvertChainToElement(ResourceFieldChainExpression chain, MutableIncludeNode rootNode) @@ -161,7 +161,7 @@ public MutableIncludeNode(RelationshipAttribute relationship) public IncludeElementExpression ToExpression() { - ImmutableArray elementChildren = Children.Values.Select(child => child.ToExpression()).ToImmutableArray(); + IImmutableSet elementChildren = Children.Values.Select(child => child.ToExpression()).ToImmutableHashSet(); return new IncludeElementExpression(_relationship, elementChildren); } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs index 669430ad99..648986fe18 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs @@ -14,14 +14,14 @@ namespace JsonApiDotNetCore.Queries.Expressions public class IncludeElementExpression : QueryExpression { public RelationshipAttribute Relationship { get; } - public IImmutableList Children { get; } + public IImmutableSet Children { get; } public IncludeElementExpression(RelationshipAttribute relationship) - : this(relationship, ImmutableArray.Empty) + : this(relationship, ImmutableHashSet.Empty) { } - public IncludeElementExpression(RelationshipAttribute relationship, IImmutableList children) + public IncludeElementExpression(RelationshipAttribute relationship, IImmutableSet children) { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(children, nameof(children)); @@ -43,7 +43,7 @@ public override string ToString() if (Children.Any()) { builder.Append('{'); - builder.Append(string.Join(",", Children.Select(child => child.ToString()))); + builder.Append(string.Join(",", Children.Select(child => child.ToString()).OrderBy(name => name))); builder.Append('}'); } @@ -64,7 +64,7 @@ public override bool Equals(object obj) var other = (IncludeElementExpression)obj; - return Relationship.Equals(other.Relationship) && Children.SequenceEqual(other.Children); + return Relationship.Equals(other.Relationship) && Children.SetEquals(other.Children); } public override int GetHashCode() diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs index 482ba0158d..632e04af30 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs @@ -16,9 +16,9 @@ public class IncludeExpression : QueryExpression public static readonly IncludeExpression Empty = new(); - public IImmutableList Elements { get; } + public IImmutableSet Elements { get; } - public IncludeExpression(IImmutableList elements) + public IncludeExpression(IImmutableSet elements) { ArgumentGuard.NotNullNorEmpty(elements, nameof(elements)); @@ -27,7 +27,7 @@ public IncludeExpression(IImmutableList elements) private IncludeExpression() { - Elements = ImmutableArray.Empty; + Elements = ImmutableHashSet.Empty; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) @@ -38,7 +38,7 @@ public override TResult Accept(QueryExpressionVisitor chains = IncludeChainConverter.GetRelationshipChains(this); - return string.Join(",", chains.Select(child => child.ToString())); + return string.Join(",", chains.Select(child => child.ToString()).OrderBy(name => name)); } public override bool Equals(object obj) @@ -55,7 +55,7 @@ public override bool Equals(object obj) var other = (IncludeExpression)obj; - return Elements.SequenceEqual(other.Elements); + return Elements.SetEquals(other.Elements); } public override int GetHashCode() diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs index bd4d1e4de8..5efff034a6 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs @@ -268,7 +268,7 @@ public override QueryExpression VisitInclude(IncludeExpression expression, TArgu { if (expression != null) { - IImmutableList newElements = VisitList(expression.Elements, argument); + IImmutableSet newElements = VisitSet(expression.Elements, argument); if (newElements.Count == 0) { @@ -286,7 +286,7 @@ public override QueryExpression VisitIncludeElement(IncludeElementExpression exp { if (expression != null) { - IImmutableList newElements = VisitList(expression.Children, argument); + IImmutableSet newElements = VisitSet(expression.Children, argument); var newExpression = new IncludeElementExpression(expression.Relationship, newElements); return newExpression.Equals(expression) ? expression : newExpression; diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index 9bfe10d6f3..4f8d957c55 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -131,7 +131,7 @@ private IncludeExpression ComposeChildren(QueryLayer topLayer, ICollection includeElements = + IImmutableSet includeElements = ProcessIncludeSet(include.Elements, topLayer, new List(), constraints); return !ReferenceEquals(includeElements, include.Elements) @@ -139,13 +139,13 @@ private IncludeExpression ComposeChildren(QueryLayer topLayer, ICollection ProcessIncludeSet(IImmutableList includeElements, QueryLayer parentLayer, + private IImmutableSet ProcessIncludeSet(IImmutableSet includeElements, QueryLayer parentLayer, ICollection parentRelationshipChain, ICollection constraints) { - IImmutableList includeElementsEvaluated = - GetIncludeElements(includeElements, parentLayer.ResourceContext) ?? ImmutableArray.Empty; + IImmutableSet includeElementsEvaluated = + GetIncludeElements(includeElements, parentLayer.ResourceContext) ?? ImmutableHashSet.Empty; - var updatesInChildren = new Dictionary>(); + var updatesInChildren = new Dictionary>(); foreach (IncludeElementExpression includeElement in includeElementsEvaluated) { @@ -187,7 +187,7 @@ private IImmutableList ProcessIncludeSet(IImmutableLis if (includeElement.Children.Any()) { - IImmutableList updatedChildren = + IImmutableSet updatedChildren = ProcessIncludeSet(includeElement.Children, child, relationshipChain, constraints); if (!ReferenceEquals(includeElement.Children, updatedChildren)) @@ -201,16 +201,16 @@ private IImmutableList ProcessIncludeSet(IImmutableLis return !updatesInChildren.Any() ? includeElementsEvaluated : ApplyIncludeElementUpdates(includeElementsEvaluated, updatesInChildren); } - private static IImmutableList ApplyIncludeElementUpdates(IImmutableList includeElements, - IDictionary> updatesInChildren) + private static IImmutableSet ApplyIncludeElementUpdates(IImmutableSet includeElements, + IDictionary> updatesInChildren) { - ImmutableArray.Builder newElementsBuilder = ImmutableArray.CreateBuilder(includeElements.Count); + ImmutableHashSet.Builder newElementsBuilder = ImmutableHashSet.CreateBuilder(); newElementsBuilder.AddRange(includeElements); - foreach ((IncludeElementExpression existingElement, IImmutableList updatedChildren) in updatesInChildren) + foreach ((IncludeElementExpression existingElement, IImmutableSet updatedChildren) in updatesInChildren) { - int existingIndex = newElementsBuilder.IndexOf(existingElement); - newElementsBuilder[existingIndex] = new IncludeElementExpression(existingElement.Relationship, updatedChildren); + newElementsBuilder.Remove(existingElement); + newElementsBuilder.Add(new IncludeElementExpression(existingElement.Relationship, updatedChildren)); } return newElementsBuilder.ToImmutable(); @@ -301,7 +301,7 @@ private IncludeExpression RewriteIncludeForSecondaryEndpoint(IncludeExpression r ? new IncludeElementExpression(secondaryRelationship, relativeInclude.Elements) : new IncludeElementExpression(secondaryRelationship); - return new IncludeExpression(ImmutableArray.Create(parentElement)); + return new IncludeExpression(ImmutableHashSet.Create(parentElement)); } private FilterExpression CreateFilterByIds(IReadOnlyCollection ids, AttrAttribute idAttribute, FilterExpression existingFilter) @@ -329,8 +329,8 @@ public QueryLayer ComposeForUpdate(TId id, ResourceContext primaryResource) { ArgumentGuard.NotNull(primaryResource, nameof(primaryResource)); - ImmutableArray includeElements = _targetedFields.Relationships - .Select(relationship => new IncludeElementExpression(relationship)).ToImmutableArray(); + IImmutableSet includeElements = _targetedFields.Relationships + .Select(relationship => new IncludeElementExpression(relationship)).ToImmutableHashSet(); AttrAttribute primaryIdAttribute = GetIdAttribute(primaryResource); @@ -405,7 +405,7 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T return new QueryLayer(leftResourceContext) { - Include = new IncludeExpression(ImmutableArray.Create(new IncludeElementExpression(hasManyRelationship))), + Include = new IncludeExpression(ImmutableHashSet.Create(new IncludeElementExpression(hasManyRelationship))), Filter = leftFilter, Projection = new Dictionary { @@ -422,7 +422,7 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T }; } - protected virtual IImmutableList GetIncludeElements(IImmutableList includeElements, + protected virtual IImmutableSet GetIncludeElements(IImmutableSet includeElements, ResourceContext resourceContext) { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs index 4fae023853..a310790fdc 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs @@ -44,7 +44,7 @@ public interface IResourceDefinition /// /// The new set of includes. Return an empty collection to remove all inclusions (never return null). /// - IImmutableList OnApplyIncludes(IImmutableList existingIncludes); + IImmutableSet OnApplyIncludes(IImmutableSet existingIncludes); /// /// Enables to extend, replace or remove a filter that is being applied on a set of this resource type. diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs index 8c804d3602..d9afc91c24 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs @@ -18,7 +18,7 @@ public interface IResourceDefinitionAccessor /// /// Invokes for the specified resource type. /// - IImmutableList OnApplyIncludes(Type resourceType, IImmutableList existingIncludes); + IImmutableSet OnApplyIncludes(Type resourceType, IImmutableSet existingIncludes); /// /// Invokes for the specified resource type. diff --git a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs index 755ce781cb..fa2206f5c4 100644 --- a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs @@ -52,7 +52,7 @@ public JsonApiResourceDefinition(IResourceGraph resourceGraph) } /// - public virtual IImmutableList OnApplyIncludes(IImmutableList existingIncludes) + public virtual IImmutableSet OnApplyIncludes(IImmutableSet existingIncludes) { return existingIncludes; } diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs index 1923c33156..fd0656e2bd 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -29,7 +29,7 @@ public ResourceDefinitionAccessor(IResourceGraph resourceGraph, IServiceProvider } /// - public IImmutableList OnApplyIncludes(Type resourceType, IImmutableList existingIncludes) + public IImmutableSet OnApplyIncludes(Type resourceType, IImmutableSet existingIncludes) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); diff --git a/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs index 24a9d85073..730adc43d4 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs @@ -65,7 +65,7 @@ public Document Convert(object model) var document = new Document(); IncludeExpression include = _evaluatedIncludeCache.Get(); - IImmutableList includeElements = include?.Elements ?? ImmutableArray.Empty; + IImmutableSet includeElements = include?.Elements ?? ImmutableHashSet.Empty; var includedCollection = new IncludedCollection(); @@ -111,7 +111,7 @@ public Document Convert(object model) return document; } - private AtomicResultObject ConvertOperation(OperationContainer operation, IImmutableList includeElements, + private AtomicResultObject ConvertOperation(OperationContainer operation, IImmutableSet includeElements, IncludedCollection includedCollection) { ResourceObject resourceObject = null; @@ -131,7 +131,7 @@ private AtomicResultObject ConvertOperation(OperationContainer operation, IImmut }; } - private ResourceObject ConvertResource(IIdentifiable resource, EndpointKind requestKind, IImmutableList includeElements, + private ResourceObject ConvertResource(IIdentifiable resource, EndpointKind requestKind, IImmutableSet includeElements, IncludedCollection includedCollection, bool isInclude) { ResourceContext resourceContext = _resourceGraph.GetResourceContext(resource.GetType()); @@ -203,7 +203,7 @@ private IDictionary ConvertAttributes(IIdentifiable resource, Re } private IDictionary ConvertRelationships(IIdentifiable resource, ResourceContext resourceContext, - IImmutableSet fieldSet, EndpointKind requestKind, IImmutableList includeElements, + IImmutableSet fieldSet, EndpointKind requestKind, IImmutableSet includeElements, IncludedCollection includedCollection) { if (fieldSet != null) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs index a40696a23a..e3f0dccfe7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs @@ -25,7 +25,7 @@ public MoonDefinition(IResourceGraph resourceGraph, IClientSettingsProvider clie _hitCounter = hitCounter; } - public override IImmutableList OnApplyIncludes(IImmutableList existingIncludes) + public override IImmutableSet OnApplyIncludes(IImmutableSet existingIncludes) { _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs index b2313bee98..512e79556e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs @@ -27,7 +27,7 @@ public PlanetDefinition(IResourceGraph resourceGraph, IClientSettingsProvider cl _hitCounter = hitCounter; } - public override IImmutableList OnApplyIncludes(IImmutableList existingIncludes) + public override IImmutableSet OnApplyIncludes(IImmutableSet existingIncludes) { _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes); diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/IncludeParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/IncludeParseTests.cs index 30af87ab5c..af2ea10a99 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/IncludeParseTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/IncludeParseTests.cs @@ -82,7 +82,7 @@ public void Reader_Read_Fails(string parameterName, string parameterValue, strin [InlineData("includes", "posts.author", "posts.author")] [InlineData("includes", "posts.comments", "posts.comments")] [InlineData("includes", "posts,posts.comments", "posts.comments")] - [InlineData("includes", "posts,posts.comments,posts.labels", "posts.comments,posts.labels")] + [InlineData("includes", "posts,posts.labels,posts.comments", "posts.comments,posts.labels")] public void Reader_Read_Succeeds(string parameterName, string parameterValue, string valueExpected) { // Act diff --git a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index c5c41a8b63..663d166f88 100644 --- a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -410,7 +410,7 @@ public Task RemoveFromToManyRelationshipAsync(GuidResource leftResource, ISet { - public IImmutableList OnApplyIncludes(IImmutableList existingIncludes) + public IImmutableSet OnApplyIncludes(IImmutableSet existingIncludes) { throw new NotImplementedException(); } @@ -498,7 +498,7 @@ public void OnSerialize(IntResource resource) [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] private sealed class GuidResourceDefinition : IResourceDefinition { - public IImmutableList OnApplyIncludes(IImmutableList existingIncludes) + public IImmutableSet OnApplyIncludes(IImmutableSet existingIncludes) { throw new NotImplementedException(); } From f8d71f20f94a153fedf93ced22733a23f330c1f8 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Oct 2021 16:01:01 +0200 Subject: [PATCH 36/49] Fixed: return Content-Length header in HEAD response https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD --- src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs | 11 ++++++----- .../Serialization/SerializationTests.cs | 4 ++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs index 0b73e2f654..fc7f2fb218 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs @@ -61,6 +61,12 @@ public async Task WriteAsync(object model, HttpContext httpContext) string responseBody = GetResponseBody(model, httpContext); + if (httpContext.Request.Method == HttpMethod.Head.Method) + { + httpContext.Response.GetTypedHeaders().ContentLength = Encoding.UTF8.GetByteCount(responseBody); + return; + } + _traceWriter.LogMessage(() => $"Sending {httpContext.Response.StatusCode} response for {httpContext.Request.Method} request at '{httpContext.Request.GetEncodedUrl()}' with body: <<{responseBody}>>"); @@ -96,11 +102,6 @@ private string GetResponseBody(object model, HttpContext httpContext) return null; } - if (httpContext.Request.Method == HttpMethod.Head.Method) - { - return null; - } - return responseBody; } #pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs index 675c9f25ad..98d412ebff 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs @@ -65,6 +65,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.Content.Headers.ContentLength.Should().BeGreaterThan(0); + responseDocument.Should().BeEmpty(); } @@ -80,6 +82,8 @@ public async Task Returns_no_body_for_failed_HEAD_request() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + httpResponse.Content.Headers.ContentLength.Should().BeGreaterThan(0); + responseDocument.Should().BeEmpty(); } From ff763f100adb4bcc7e7b232f18ae7e3029d24a10 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Oct 2021 16:08:29 +0200 Subject: [PATCH 37/49] Reorganized JADNC.Serialization namespace --- benchmarks/Deserialization/DeserializationBenchmarkBase.cs | 2 +- benchmarks/Serialization/SerializationBenchmarkBase.cs | 3 +-- .../Configuration/JsonApiApplicationBuilder.cs | 6 +++--- src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs | 2 +- src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs | 2 +- .../Serialization/JsonConverters/ResourceObjectConverter.cs | 1 + .../Adapters}/AtomicOperationObjectAdapter.cs | 2 +- .../Adapters}/AtomicReferenceAdapter.cs | 2 +- .../Adapters}/AtomicReferenceResult.cs | 2 +- .../Adapters}/BaseDataAdapter.cs | 2 +- .../Adapters}/DocumentAdapter.cs | 2 +- .../Adapters}/DocumentInOperationsRequestAdapter.cs | 2 +- .../DocumentInResourceOrRelationshipRequestAdapter.cs | 2 +- .../Adapters}/IAtomicOperationObjectAdapter.cs | 2 +- .../Adapters}/IAtomicReferenceAdapter.cs | 2 +- .../Adapters}/IDocumentAdapter.cs | 2 +- .../Adapters}/IDocumentInOperationsRequestAdapter.cs | 2 +- .../IDocumentInResourceOrRelationshipRequestAdapter.cs | 2 +- .../Adapters}/IRelationshipDataAdapter.cs | 2 +- .../Adapters}/IResourceDataAdapter.cs | 2 +- .../Adapters}/IResourceDataInOperationsRequestAdapter.cs | 2 +- .../Adapters}/IResourceIdentifierObjectAdapter.cs | 2 +- .../Adapters}/IResourceObjectAdapter.cs | 2 +- .../Adapters}/JsonElementConstraint.cs | 2 +- .../Adapters}/RelationshipDataAdapter.cs | 2 +- .../Adapters}/RequestAdapterPosition.cs | 2 +- .../Adapters}/RequestAdapterState.cs | 2 +- .../Adapters}/ResourceDataAdapter.cs | 2 +- .../Adapters}/ResourceDataInOperationsRequestAdapter.cs | 2 +- .../Adapters}/ResourceIdentifierObjectAdapter.cs | 2 +- .../Adapters}/ResourceIdentityAdapter.cs | 2 +- .../Adapters}/ResourceIdentityRequirements.cs | 2 +- .../Adapters}/ResourceObjectAdapter.cs | 2 +- .../Serialization/{ => Request}/IJsonApiReader.cs | 2 +- .../Serialization/{ => Request}/JsonApiReader.cs | 4 ++-- .../Serialization/{ => Request}/JsonInvalidAttributeInfo.cs | 2 +- .../Serialization/{ => Request}/ModelConversionException.cs | 4 ++-- .../Serialization/{ => Response}/ETagGenerator.cs | 2 +- .../Serialization/{ => Response}/EmptyResponseMeta.cs | 2 +- .../Serialization/{ => Response}/FingerprintGenerator.cs | 2 +- .../Serialization/{ => Response}/IETagGenerator.cs | 2 +- .../Serialization/{ => Response}/IFingerprintGenerator.cs | 2 +- .../Serialization/{ => Response}/IJsonApiWriter.cs | 2 +- .../Serialization/{Building => Response}/ILinkBuilder.cs | 2 +- .../Serialization/{Building => Response}/IMetaBuilder.cs | 2 +- .../Serialization/{ => Response}/IResponseMeta.cs | 2 +- .../Serialization/{ => Response}/IResponseModelAdapter.cs | 2 +- .../Serialization/{ => Response}/JsonApiWriter.cs | 2 +- .../Serialization/{Building => Response}/LinkBuilder.cs | 2 +- .../Serialization/{Building => Response}/MetaBuilder.cs | 2 +- .../Serialization/{ => Response}/ResponseModelAdapter.cs | 3 +-- .../AtomicOperations/Meta/AtomicResponseMeta.cs | 2 +- .../AtomicOperations/Meta/AtomicResponseMetaTests.cs | 2 +- .../IntegrationTests/Meta/ResponseMetaTests.cs | 2 +- .../IntegrationTests/Meta/SupportResponseMeta.cs | 2 +- .../UnitTests/Links/LinkInclusionTests.cs | 2 +- .../UnitTests/Serialization/InputConversionTests.cs | 2 +- 57 files changed, 61 insertions(+), 62 deletions(-) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/AtomicOperationObjectAdapter.cs (99%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/AtomicReferenceAdapter.cs (97%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/AtomicReferenceResult.cs (94%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/BaseDataAdapter.cs (97%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/DocumentAdapter.cs (97%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/DocumentInOperationsRequestAdapter.cs (98%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/DocumentInResourceOrRelationshipRequestAdapter.cs (98%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/IAtomicOperationObjectAdapter.cs (89%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/IAtomicReferenceAdapter.cs (91%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/IDocumentAdapter.cs (95%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/IDocumentInOperationsRequestAdapter.cs (90%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/IDocumentInResourceOrRelationshipRequestAdapter.cs (88%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/IRelationshipDataAdapter.cs (94%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/IResourceDataAdapter.cs (89%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/IResourceDataInOperationsRequestAdapter.cs (90%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/IResourceIdentifierObjectAdapter.cs (91%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/IResourceObjectAdapter.cs (92%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/JsonElementConstraint.cs (88%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/RelationshipDataAdapter.cs (98%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/RequestAdapterPosition.cs (96%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/RequestAdapterState.cs (97%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/ResourceDataAdapter.cs (97%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/ResourceDataInOperationsRequestAdapter.cs (95%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/ResourceIdentifierObjectAdapter.cs (94%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/ResourceIdentityAdapter.cs (99%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/ResourceIdentityRequirements.cs (95%) rename src/JsonApiDotNetCore/Serialization/{RequestAdapters => Request/Adapters}/ResourceObjectAdapter.cs (99%) rename src/JsonApiDotNetCore/Serialization/{ => Request}/IJsonApiReader.cs (91%) rename src/JsonApiDotNetCore/Serialization/{ => Request}/JsonApiReader.cs (97%) rename src/JsonApiDotNetCore/Serialization/{ => Request}/JsonInvalidAttributeInfo.cs (95%) rename src/JsonApiDotNetCore/Serialization/{ => Request}/ModelConversionException.cs (89%) rename src/JsonApiDotNetCore/Serialization/{ => Response}/ETagGenerator.cs (93%) rename src/JsonApiDotNetCore/Serialization/{ => Response}/EmptyResponseMeta.cs (83%) rename src/JsonApiDotNetCore/Serialization/{ => Response}/FingerprintGenerator.cs (97%) rename src/JsonApiDotNetCore/Serialization/{ => Response}/IETagGenerator.cs (93%) rename src/JsonApiDotNetCore/Serialization/{ => Response}/IFingerprintGenerator.cs (89%) rename src/JsonApiDotNetCore/Serialization/{ => Response}/IJsonApiWriter.cs (89%) rename src/JsonApiDotNetCore/Serialization/{Building => Response}/ILinkBuilder.cs (94%) rename src/JsonApiDotNetCore/Serialization/{Building => Response}/IMetaBuilder.cs (92%) rename src/JsonApiDotNetCore/Serialization/{ => Response}/IResponseMeta.cs (92%) rename src/JsonApiDotNetCore/Serialization/{ => Response}/IResponseModelAdapter.cs (96%) rename src/JsonApiDotNetCore/Serialization/{ => Response}/JsonApiWriter.cs (99%) rename src/JsonApiDotNetCore/Serialization/{Building => Response}/LinkBuilder.cs (99%) rename src/JsonApiDotNetCore/Serialization/{Building => Response}/MetaBuilder.cs (97%) rename src/JsonApiDotNetCore/Serialization/{ => Response}/ResponseModelAdapter.cs (99%) diff --git a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs index 59639512bb..e362b059bd 100644 --- a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs +++ b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs @@ -8,7 +8,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.JsonConverters; -using JsonApiDotNetCore.Serialization.RequestAdapters; +using JsonApiDotNetCore.Serialization.Request.Adapters; using Microsoft.Extensions.Logging.Abstractions; namespace Benchmarks.Deserialization diff --git a/benchmarks/Serialization/SerializationBenchmarkBase.cs b/benchmarks/Serialization/SerializationBenchmarkBase.cs index f7c578b0d5..f7de226ed2 100644 --- a/benchmarks/Serialization/SerializationBenchmarkBase.cs +++ b/benchmarks/Serialization/SerializationBenchmarkBase.cs @@ -14,9 +14,8 @@ using JsonApiDotNetCore.QueryStrings; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.Serialization.Response; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 0547af7aba..d911290ace 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -10,10 +10,10 @@ using JsonApiDotNetCore.QueryStrings.Internal; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.JsonConverters; -using JsonApiDotNetCore.Serialization.RequestAdapters; +using JsonApiDotNetCore.Serialization.Request; +using JsonApiDotNetCore.Serialization.Request.Adapters; +using JsonApiDotNetCore.Serialization.Response; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs index 9f43df18fd..d22904bebc 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Request; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.DependencyInjection; diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs index 07e7163b25..93d531dc58 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Response; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.DependencyInjection; diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs index 8b8d3c7fa8..14bf2f3fcb 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs @@ -7,6 +7,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.Serialization.Request; namespace JsonApiDotNetCore.Serialization.JsonConverters { diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicOperationObjectAdapter.cs similarity index 99% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicOperationObjectAdapter.cs index 5b9dcd3202..54f3b1cceb 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicOperationObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicOperationObjectAdapter.cs @@ -5,7 +5,7 @@ using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// public sealed class AtomicOperationObjectAdapter : IAtomicOperationObjectAdapter diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceAdapter.cs similarity index 97% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceAdapter.cs index 95ff4f56e6..7c3c5392e6 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceAdapter.cs @@ -4,7 +4,7 @@ using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// public sealed class AtomicReferenceAdapter : ResourceIdentityAdapter, IAtomicReferenceAdapter diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceResult.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceResult.cs similarity index 94% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceResult.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceResult.cs index 43343054c3..03879b2b4d 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceResult.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceResult.cs @@ -3,7 +3,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// The result of validating and converting "ref" in an entry of an atomic:operations request. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/BaseDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/BaseDataAdapter.cs similarity index 97% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/BaseDataAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/BaseDataAdapter.cs index fa22de3d32..2dffca2653 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/BaseDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/BaseDataAdapter.cs @@ -1,7 +1,7 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Contains shared assertions for derived types. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentAdapter.cs similarity index 97% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentAdapter.cs index ff8f99c7d3..4877f500b7 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentAdapter.cs @@ -2,7 +2,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// public sealed class DocumentAdapter : IDocumentAdapter diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentInOperationsRequestAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInOperationsRequestAdapter.cs similarity index 98% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentInOperationsRequestAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInOperationsRequestAdapter.cs index 4633bd803a..0c283e9bf0 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentInOperationsRequestAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInOperationsRequestAdapter.cs @@ -4,7 +4,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// public sealed class DocumentInOperationsRequestAdapter : IDocumentInOperationsRequestAdapter diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentInResourceOrRelationshipRequestAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInResourceOrRelationshipRequestAdapter.cs similarity index 98% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentInResourceOrRelationshipRequestAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInResourceOrRelationshipRequestAdapter.cs index abb0c91ed8..2dd491edce 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/DocumentInResourceOrRelationshipRequestAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInResourceOrRelationshipRequestAdapter.cs @@ -4,7 +4,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// public sealed class DocumentInResourceOrRelationshipRequestAdapter : IDocumentInResourceOrRelationshipRequestAdapter diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicOperationObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IAtomicOperationObjectAdapter.cs similarity index 89% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicOperationObjectAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/IAtomicOperationObjectAdapter.cs index a1567896d2..5fb2c1d680 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicOperationObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IAtomicOperationObjectAdapter.cs @@ -1,7 +1,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Validates and converts a single operation inside an atomic:operations request. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicReferenceAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IAtomicReferenceAdapter.cs similarity index 91% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicReferenceAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/IAtomicReferenceAdapter.cs index 7d47288608..bd4a12b2de 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IAtomicReferenceAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IAtomicReferenceAdapter.cs @@ -1,6 +1,6 @@ using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Validates and converts a 'ref' element in an entry of an atomic:operations request. It appears in most kinds of operations and typically indicates diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentAdapter.cs similarity index 95% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentAdapter.cs index 4343e3bb49..3f13b25685 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentAdapter.cs @@ -1,6 +1,6 @@ using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// The entry point for validating and converting the deserialized from the request body into a model. The produced models are diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentInOperationsRequestAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentInOperationsRequestAdapter.cs similarity index 90% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentInOperationsRequestAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentInOperationsRequestAdapter.cs index 5d96e4d163..de39fa6c91 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentInOperationsRequestAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentInOperationsRequestAdapter.cs @@ -2,7 +2,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Validates and converts a belonging to an atomic:operations request. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentInResourceOrRelationshipRequestAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentInResourceOrRelationshipRequestAdapter.cs similarity index 88% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentInResourceOrRelationshipRequestAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentInResourceOrRelationshipRequestAdapter.cs index 4ea2dcdd28..da6222e166 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IDocumentInResourceOrRelationshipRequestAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IDocumentInResourceOrRelationshipRequestAdapter.cs @@ -1,6 +1,6 @@ using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Validates and converts a belonging to a resource or relationship request. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IRelationshipDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IRelationshipDataAdapter.cs similarity index 94% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IRelationshipDataAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/IRelationshipDataAdapter.cs index 28f8d77989..cc4da5bc5e 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IRelationshipDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IRelationshipDataAdapter.cs @@ -1,7 +1,7 @@ using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Validates and converts the data from a relationship. It appears in a relationship request, in the relationships of a POST/PATCH resource request, in diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceDataAdapter.cs similarity index 89% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceDataAdapter.cs index 197f0d0607..e7dd737cfb 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceDataAdapter.cs @@ -1,7 +1,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Validates and converts the data from a resource in a POST/PATCH resource request. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataInOperationsRequestAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceDataInOperationsRequestAdapter.cs similarity index 90% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataInOperationsRequestAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceDataInOperationsRequestAdapter.cs index 95c539ea1b..38a841d45d 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceDataInOperationsRequestAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceDataInOperationsRequestAdapter.cs @@ -1,7 +1,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Validates and converts the data from an entry in an atomic:operations request that creates or updates a resource. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceIdentifierObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceIdentifierObjectAdapter.cs similarity index 91% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceIdentifierObjectAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceIdentifierObjectAdapter.cs index e9e6383d40..3105143908 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceIdentifierObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceIdentifierObjectAdapter.cs @@ -1,7 +1,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Validates and converts a . It appears in the data object(s) of a relationship. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceObjectAdapter.cs similarity index 92% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceObjectAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceObjectAdapter.cs index 578009434c..e975e12604 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/IResourceObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceObjectAdapter.cs @@ -2,7 +2,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Validates and converts a . It appears in a POST/PATCH resource request and an entry in an atomic:operations request that diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/JsonElementConstraint.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/JsonElementConstraint.cs similarity index 88% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/JsonElementConstraint.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/JsonElementConstraint.cs index d2979809db..5cb4708fd3 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/JsonElementConstraint.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/JsonElementConstraint.cs @@ -1,6 +1,6 @@ using JetBrains.Annotations; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Lists constraints for the presence or absence of a JSON element. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs similarity index 98% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs index 5d2382f91d..be6e571b5f 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs @@ -7,7 +7,7 @@ using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// public sealed class RelationshipDataAdapter : BaseDataAdapter, IRelationshipDataAdapter diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterPosition.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/RequestAdapterPosition.cs similarity index 96% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterPosition.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/RequestAdapterPosition.cs index 992130f182..4c2d34b28d 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterPosition.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/RequestAdapterPosition.cs @@ -4,7 +4,7 @@ using System.Text; using JetBrains.Annotations; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Tracks the location within an object tree when validating and converting a request body. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterState.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/RequestAdapterState.cs similarity index 97% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterState.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/RequestAdapterState.cs index 80ab398dd7..2730bcbf9b 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/RequestAdapterState.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/RequestAdapterState.cs @@ -4,7 +4,7 @@ using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Tracks state while adapting objects from into the shape that controller actions accept. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataAdapter.cs similarity index 97% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataAdapter.cs index 150be2d4b1..c1e39a703c 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataAdapter.cs @@ -3,7 +3,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// public class ResourceDataAdapter : BaseDataAdapter, IResourceDataAdapter diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataInOperationsRequestAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataInOperationsRequestAdapter.cs similarity index 95% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataInOperationsRequestAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataInOperationsRequestAdapter.cs index 401f780b58..68735d84a8 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceDataInOperationsRequestAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataInOperationsRequestAdapter.cs @@ -2,7 +2,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// public sealed class ResourceDataInOperationsRequestAdapter : ResourceDataAdapter, IResourceDataInOperationsRequestAdapter diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentifierObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentifierObjectAdapter.cs similarity index 94% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentifierObjectAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentifierObjectAdapter.cs index acea2c1a74..029c7c6f8c 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentifierObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentifierObjectAdapter.cs @@ -2,7 +2,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// public sealed class ResourceIdentifierObjectAdapter : ResourceIdentityAdapter, IResourceIdentifierObjectAdapter diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs similarity index 99% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs index 05431d6309..5767711769 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs @@ -6,7 +6,7 @@ using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Base class for validating and converting objects that represent an identity. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityRequirements.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs similarity index 95% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityRequirements.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs index 611aa8135d..a9386be562 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityRequirements.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs @@ -2,7 +2,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// /// Defines requirements to validate an instance against. diff --git a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs similarity index 99% rename from src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs index 91dca72057..5ac7021641 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs @@ -7,7 +7,7 @@ using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.RequestAdapters +namespace JsonApiDotNetCore.Serialization.Request.Adapters { /// public sealed class ResourceObjectAdapter : ResourceIdentityAdapter, IResourceObjectAdapter diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/Request/IJsonApiReader.cs similarity index 91% rename from src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs rename to src/JsonApiDotNetCore/Serialization/Request/IJsonApiReader.cs index 9aecc8b0b2..aab4712844 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/IJsonApiReader.cs @@ -2,7 +2,7 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.Http; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Request { /// /// Deserializes the incoming JSON:API request body and converts it to models, which are passed to controller actions by ASP.NET Core on `FromBody` diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs similarity index 97% rename from src/JsonApiDotNetCore/Serialization/JsonApiReader.cs rename to src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs index 7d1de70925..96831014c3 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs @@ -9,13 +9,13 @@ using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCore.Serialization.RequestAdapters; +using JsonApiDotNetCore.Serialization.Request.Adapters; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Request { /// public sealed class JsonApiReader : IJsonApiReader diff --git a/src/JsonApiDotNetCore/Serialization/JsonInvalidAttributeInfo.cs b/src/JsonApiDotNetCore/Serialization/Request/JsonInvalidAttributeInfo.cs similarity index 95% rename from src/JsonApiDotNetCore/Serialization/JsonInvalidAttributeInfo.cs rename to src/JsonApiDotNetCore/Serialization/Request/JsonInvalidAttributeInfo.cs index 037eaf18af..5c448613a6 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonInvalidAttributeInfo.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/JsonInvalidAttributeInfo.cs @@ -1,7 +1,7 @@ using System; using System.Text.Json; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Request { /// /// A sentinel value that is temporarily stored in the attributes dictionary to postpone producing an error. diff --git a/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs b/src/JsonApiDotNetCore/Serialization/Request/ModelConversionException.cs similarity index 89% rename from src/JsonApiDotNetCore/Serialization/ModelConversionException.cs rename to src/JsonApiDotNetCore/Serialization/Request/ModelConversionException.cs index ed0315a918..c9922e855b 100644 --- a/src/JsonApiDotNetCore/Serialization/ModelConversionException.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/ModelConversionException.cs @@ -1,9 +1,9 @@ using System; using System.Net; using JetBrains.Annotations; -using JsonApiDotNetCore.Serialization.RequestAdapters; +using JsonApiDotNetCore.Serialization.Request.Adapters; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Request { /// /// The error that is thrown when unable to convert a deserialized request body to an ASP.NET model. diff --git a/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs b/src/JsonApiDotNetCore/Serialization/Response/ETagGenerator.cs similarity index 93% rename from src/JsonApiDotNetCore/Serialization/ETagGenerator.cs rename to src/JsonApiDotNetCore/Serialization/Response/ETagGenerator.cs index bc1a7f3e49..f2a78adb65 100644 --- a/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/ETagGenerator.cs @@ -1,6 +1,6 @@ using Microsoft.Net.Http.Headers; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Response { /// internal sealed class ETagGenerator : IETagGenerator diff --git a/src/JsonApiDotNetCore/Serialization/EmptyResponseMeta.cs b/src/JsonApiDotNetCore/Serialization/Response/EmptyResponseMeta.cs similarity index 83% rename from src/JsonApiDotNetCore/Serialization/EmptyResponseMeta.cs rename to src/JsonApiDotNetCore/Serialization/Response/EmptyResponseMeta.cs index 592a752926..a06e4b76c3 100644 --- a/src/JsonApiDotNetCore/Serialization/EmptyResponseMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/EmptyResponseMeta.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Response { /// public sealed class EmptyResponseMeta : IResponseMeta diff --git a/src/JsonApiDotNetCore/Serialization/FingerprintGenerator.cs b/src/JsonApiDotNetCore/Serialization/Response/FingerprintGenerator.cs similarity index 97% rename from src/JsonApiDotNetCore/Serialization/FingerprintGenerator.cs rename to src/JsonApiDotNetCore/Serialization/Response/FingerprintGenerator.cs index 3c1083dfbe..9835bf5de3 100644 --- a/src/JsonApiDotNetCore/Serialization/FingerprintGenerator.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/FingerprintGenerator.cs @@ -3,7 +3,7 @@ using System.Security.Cryptography; using System.Text; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Response { /// internal sealed class FingerprintGenerator : IFingerprintGenerator diff --git a/src/JsonApiDotNetCore/Serialization/IETagGenerator.cs b/src/JsonApiDotNetCore/Serialization/Response/IETagGenerator.cs similarity index 93% rename from src/JsonApiDotNetCore/Serialization/IETagGenerator.cs rename to src/JsonApiDotNetCore/Serialization/Response/IETagGenerator.cs index 5aa3abf759..5fc5070129 100644 --- a/src/JsonApiDotNetCore/Serialization/IETagGenerator.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/IETagGenerator.cs @@ -1,6 +1,6 @@ using Microsoft.Net.Http.Headers; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Response { /// /// Provides generation of an ETag HTTP response header. diff --git a/src/JsonApiDotNetCore/Serialization/IFingerprintGenerator.cs b/src/JsonApiDotNetCore/Serialization/Response/IFingerprintGenerator.cs similarity index 89% rename from src/JsonApiDotNetCore/Serialization/IFingerprintGenerator.cs rename to src/JsonApiDotNetCore/Serialization/Response/IFingerprintGenerator.cs index 51fafaf650..7b18e7db19 100644 --- a/src/JsonApiDotNetCore/Serialization/IFingerprintGenerator.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/IFingerprintGenerator.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using JetBrains.Annotations; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Response { /// /// Provides a method to generate a fingerprint for a collection of string values. diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/Response/IJsonApiWriter.cs similarity index 89% rename from src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs rename to src/JsonApiDotNetCore/Serialization/Response/IJsonApiWriter.cs index 453be1bdf0..e8c0e6dab4 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/IJsonApiWriter.cs @@ -2,7 +2,7 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.Http; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Response { /// /// Serializes ASP.NET models into the outgoing JSON:API response body. diff --git a/src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/ILinkBuilder.cs similarity index 94% rename from src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Response/ILinkBuilder.cs index 86462aee54..f49859f132 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/ILinkBuilder.cs @@ -2,7 +2,7 @@ using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.Building +namespace JsonApiDotNetCore.Serialization.Response { /// /// Builds resource object links and relationship object links. diff --git a/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/IMetaBuilder.cs similarity index 92% rename from src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Response/IMetaBuilder.cs index 1e668feca5..c94b0150da 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/IMetaBuilder.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using JetBrains.Annotations; -namespace JsonApiDotNetCore.Serialization.Building +namespace JsonApiDotNetCore.Serialization.Response { /// /// Builds the top-level meta object. diff --git a/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs b/src/JsonApiDotNetCore/Serialization/Response/IResponseMeta.cs similarity index 92% rename from src/JsonApiDotNetCore/Serialization/IResponseMeta.cs rename to src/JsonApiDotNetCore/Serialization/Response/IResponseMeta.cs index 2561da2543..ca13ceaad2 100644 --- a/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/IResponseMeta.cs @@ -2,7 +2,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Response { /// /// Provides a method to obtain global JSON:API meta, which is added at top-level to a response . Use diff --git a/src/JsonApiDotNetCore/Serialization/IResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/Response/IResponseModelAdapter.cs similarity index 96% rename from src/JsonApiDotNetCore/Serialization/IResponseModelAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Response/IResponseModelAdapter.cs index 7fed72c401..960d541cd4 100644 --- a/src/JsonApiDotNetCore/Serialization/IResponseModelAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/IResponseModelAdapter.cs @@ -1,6 +1,6 @@ using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Response { /// /// Converts the produced model from an ASP.NET controller action into a , ready to be serialized as the response body. diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs similarity index 99% rename from src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs rename to src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs index fc7f2fb218..57d33276a1 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Response { /// public sealed class JsonApiWriter : IJsonApiWriter diff --git a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs similarity index 99% rename from src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs index 9dde02d308..4b0231d186 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs @@ -16,7 +16,7 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Routing; -namespace JsonApiDotNetCore.Serialization.Building +namespace JsonApiDotNetCore.Serialization.Response { [PublicAPI] public class LinkBuilder : ILinkBuilder diff --git a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/MetaBuilder.cs similarity index 97% rename from src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Response/MetaBuilder.cs index dcddb2aa53..265154cca4 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/MetaBuilder.cs @@ -4,7 +4,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; -namespace JsonApiDotNetCore.Serialization.Building +namespace JsonApiDotNetCore.Serialization.Response { /// [PublicAPI] diff --git a/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs similarity index 99% rename from src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs rename to src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs index 730adc43d4..8cce46d53e 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseModelAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs @@ -12,10 +12,9 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Resources.Internal; -using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Response { /// public sealed class ResponseModelAdapter : IResponseModelAdapter diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs index c6fede0077..a1f67e0bb2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Response; namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations.Meta { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs index f775ea6c79..e1469b3454 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs @@ -5,8 +5,8 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.Serialization.Response; using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs index 0c698644da..ed5e3da83f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Response; using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/SupportResponseMeta.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/SupportResponseMeta.cs index dccf338f22..1f7eee1262 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/SupportResponseMeta.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/SupportResponseMeta.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Response; namespace JsonApiDotNetCoreTests.IntegrationTests.Meta { diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs index deceb19902..c3b81522ac 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs @@ -6,8 +6,8 @@ using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.Serialization.Response; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Xunit; diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs index 1b479e2684..5c63169ae9 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs @@ -10,7 +10,7 @@ using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.JsonConverters; using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCore.Serialization.RequestAdapters; +using JsonApiDotNetCore.Serialization.Request.Adapters; using Microsoft.Extensions.Logging.Abstractions; using Xunit; From 80fe2b267c6faa63e1e0586fe74493ef4b9185c5 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Oct 2021 16:37:21 +0200 Subject: [PATCH 38/49] Created custom exception for remaining errors --- .../AtomicOperations/LocalIdTracker.cs | 26 +++---------------- .../Errors/DuplicateLocalIdValueException.cs | 22 ++++++++++++++++ .../IncompatibleLocalIdTypeException.cs | 22 ++++++++++++++++ .../Errors/LocalIdSingleOperationException.cs | 22 ++++++++++++++++ .../Errors/UnknownLocalIdValueException.cs | 22 ++++++++++++++++ .../LocalIds/AtomicLocalIdTests.cs | 12 ++++----- 6 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 src/JsonApiDotNetCore/Errors/DuplicateLocalIdValueException.cs create mode 100644 src/JsonApiDotNetCore/Errors/IncompatibleLocalIdTypeException.cs create mode 100644 src/JsonApiDotNetCore/Errors/LocalIdSingleOperationException.cs create mode 100644 src/JsonApiDotNetCore/Errors/UnknownLocalIdValueException.cs diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs index 744a03a9e8..972440c4bc 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Net; using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.AtomicOperations { @@ -32,11 +30,7 @@ private void AssertIsNotDeclared(string localId) { if (_idsTracked.ContainsKey(localId)) { - 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." - }); + throw new DuplicateLocalIdValueException(localId); } } @@ -75,11 +69,7 @@ public string GetValue(string localId, string resourceType) if (item.ServerId == null) { - 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." - }); + throw new LocalIdSingleOperationException(localId); } return item.ServerId; @@ -89,11 +79,7 @@ private void AssertIsDeclared(string localId) { if (!_idsTracked.ContainsKey(localId)) { - 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." - }); + throw new UnknownLocalIdValueException(localId); } } @@ -101,11 +87,7 @@ private static void AssertSameResourceType(string currentType, string declaredTy { if (declaredType != currentType) { - 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}'." - }); + throw new IncompatibleLocalIdTypeException(localId, declaredType, currentType); } } diff --git a/src/JsonApiDotNetCore/Errors/DuplicateLocalIdValueException.cs b/src/JsonApiDotNetCore/Errors/DuplicateLocalIdValueException.cs new file mode 100644 index 0000000000..ebf9a2fadd --- /dev/null +++ b/src/JsonApiDotNetCore/Errors/DuplicateLocalIdValueException.cs @@ -0,0 +1,22 @@ +using System.Net; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Errors +{ + /// + /// The error that is thrown when assigning a local ID that was already assigned in an earlier operation. + /// + [PublicAPI] + public sealed class DuplicateLocalIdValueException : JsonApiException + { + public DuplicateLocalIdValueException(string localId) + : base(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." + }) + { + } + } +} diff --git a/src/JsonApiDotNetCore/Errors/IncompatibleLocalIdTypeException.cs b/src/JsonApiDotNetCore/Errors/IncompatibleLocalIdTypeException.cs new file mode 100644 index 0000000000..9b2e46357c --- /dev/null +++ b/src/JsonApiDotNetCore/Errors/IncompatibleLocalIdTypeException.cs @@ -0,0 +1,22 @@ +using System.Net; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Errors +{ + /// + /// The error that is thrown when referencing a local ID that was assigned to a different resource type. + /// + [PublicAPI] + public sealed class IncompatibleLocalIdTypeException : JsonApiException + { + public IncompatibleLocalIdTypeException(string localId, string declaredType, string currentType) + : base(new ErrorObject(HttpStatusCode.BadRequest) + { + Title = "Incompatible type in Local ID usage.", + Detail = $"Local ID '{localId}' belongs to resource type '{declaredType}' instead of '{currentType}'." + }) + { + } + } +} diff --git a/src/JsonApiDotNetCore/Errors/LocalIdSingleOperationException.cs b/src/JsonApiDotNetCore/Errors/LocalIdSingleOperationException.cs new file mode 100644 index 0000000000..8dfa1bd842 --- /dev/null +++ b/src/JsonApiDotNetCore/Errors/LocalIdSingleOperationException.cs @@ -0,0 +1,22 @@ +using System.Net; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Errors +{ + /// + /// The error that is thrown when assigning and referencing a local ID within the same operation. + /// + [PublicAPI] + public sealed class LocalIdSingleOperationException : JsonApiException + { + public LocalIdSingleOperationException(string localId) + : base(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." + }) + { + } + } +} diff --git a/src/JsonApiDotNetCore/Errors/UnknownLocalIdValueException.cs b/src/JsonApiDotNetCore/Errors/UnknownLocalIdValueException.cs new file mode 100644 index 0000000000..418acac662 --- /dev/null +++ b/src/JsonApiDotNetCore/Errors/UnknownLocalIdValueException.cs @@ -0,0 +1,22 @@ +using System.Net; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Errors +{ + /// + /// The error that is thrown when referencing a local ID that hasn't been assigned. + /// + [PublicAPI] + public sealed class UnknownLocalIdValueException : JsonApiException + { + public UnknownLocalIdValueException(string localId) + : base(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." + }) + { + } + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs index 036cb640af..a683635358 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs @@ -2098,7 +2098,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_same_operation() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Type mismatch in local ID usage."); + error.Title.Should().Be("Incompatible type in Local ID usage."); error.Detail.Should().Be("Local ID 'track-1' belongs to resource type 'musicTracks' instead of 'recordCompanies'."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); } @@ -2155,7 +2155,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_ref() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Type mismatch in local ID usage."); + error.Title.Should().Be("Incompatible type in Local ID usage."); error.Detail.Should().Be("Local ID 'company-1' belongs to resource type 'recordCompanies' instead of 'musicTracks'."); error.Source.Pointer.Should().Be("/atomic:operations[2]"); } @@ -2215,7 +2215,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_data_element() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Type mismatch in local ID usage."); + error.Title.Should().Be("Incompatible type in Local ID usage."); error.Detail.Should().Be("Local ID 'performer-1' belongs to resource type 'performers' instead of 'playlists'."); error.Source.Pointer.Should().Be("/atomic:operations[2]"); } @@ -2289,7 +2289,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Type mismatch in local ID usage."); + error.Title.Should().Be("Incompatible type in Local ID usage."); error.Detail.Should().Be("Local ID 'company-1' belongs to resource type 'recordCompanies' instead of 'performers'."); error.Source.Pointer.Should().Be("/atomic:operations[2]"); } @@ -2362,7 +2362,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Type mismatch in local ID usage."); + error.Title.Should().Be("Incompatible type in Local ID usage."); error.Detail.Should().Be("Local ID 'playlist-1' belongs to resource type 'playlists' instead of 'recordCompanies'."); error.Source.Pointer.Should().Be("/atomic:operations[2]"); } @@ -2432,7 +2432,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Type mismatch in local ID usage."); + error.Title.Should().Be("Incompatible type in Local ID usage."); error.Detail.Should().Be("Local ID 'performer-1' belongs to resource type 'performers' instead of 'musicTracks'."); error.Source.Pointer.Should().Be("/atomic:operations[2]"); } From f6caf3aa29b6275837976269a3f8c0d31a00346d Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Oct 2021 17:14:23 +0200 Subject: [PATCH 39/49] Fixed: call ResourceDefinition.OnApplyIncludes for all children, even when empty --- .../Queries/Internal/QueryLayerComposer.cs | 13 ++--- .../Reading/ResourceDefinitionReadTests.cs | 51 ++++++++++++++++++- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index 4f8d957c55..c49e47d0b0 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -185,15 +185,12 @@ private IImmutableSet ProcessIncludeSet(IImmutableSet< parentLayer.Projection.Add(includeElement.Relationship, child); - if (includeElement.Children.Any()) - { - IImmutableSet updatedChildren = - ProcessIncludeSet(includeElement.Children, child, relationshipChain, constraints); + IImmutableSet updatedChildren = + ProcessIncludeSet(includeElement.Children, child, relationshipChain, constraints); - if (!ReferenceEquals(includeElement.Children, updatedChildren)) - { - updatesInChildren.Add(includeElement, updatedChildren); - } + if (!ReferenceEquals(includeElement.Children, updatedChildren)) + { + updatesInChildren.Add(includeElement, updatedChildren); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs index 0018ece30e..aa837c689b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs @@ -124,7 +124,56 @@ await _testContext.RunOnDatabaseAsync(async dbContext => hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { - (typeof(Moon), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes) + (typeof(Moon), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes), + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes) + }, options => options.WithStrictOrdering()); + } + + [Fact] + public async Task Include_from_included_resource_definition_is_added() + { + // Arrange + var hitCounter = _testContext.Factory.Services.GetRequiredService(); + + var settingsProvider = (TestClientSettingsProvider)_testContext.Factory.Services.GetRequiredService(); + settingsProvider.AutoIncludeOrbitingPlanetForMoons(); + + Planet planet = _fakers.Planet.Generate(); + planet.Moons = _fakers.Moon.Generate(1).ToHashSet(); + planet.Moons.ElementAt(0).OrbitsAround = _fakers.Planet.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Planets.Add(planet); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/planets/{planet.StringId}?include=moons"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.SingleValue.Should().NotBeNull(); + + responseDocument.Included.Should().HaveCount(2); + + responseDocument.Included[0].Type.Should().Be("moons"); + responseDocument.Included[0].Id.Should().Be(planet.Moons.ElementAt(0).StringId); + responseDocument.Included[0].Attributes["name"].Should().Be(planet.Moons.ElementAt(0).Name); + + responseDocument.Included[1].Type.Should().Be("planets"); + responseDocument.Included[1].Id.Should().Be(planet.Moons.ElementAt(0).OrbitsAround.StringId); + responseDocument.Included[1].Attributes["publicName"].Should().Be(planet.Moons.ElementAt(0).OrbitsAround.PublicName); + + hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] + { + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyFilter), + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes), + (typeof(Moon), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes), + (typeof(Planet), ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes) }, options => options.WithStrictOrdering()); } From 0efde1b3ec90977639e3c3b6622fcb5790e845aa Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 7 Oct 2021 12:19:24 +0200 Subject: [PATCH 40/49] Renamed ResourceContext to ResourceType and exposed it through relationship left/right. This enabled to reduce many resource graph lookups from the codebase. --- .../DeserializationBenchmarkBase.cs | 4 +- .../ResourceDeserializationBenchmarks.cs | 2 +- benchmarks/Query/QueryParserBenchmarks.cs | 2 +- .../OperationsSerializationBenchmarks.cs | 4 +- .../ResourceSerializationBenchmarks.cs | 14 +- .../SerializationBenchmarkBase.cs | 26 ++-- docs/getting-started/step-by-step.md | 10 +- docs/usage/extensibility/repositories.md | 8 +- .../extensibility/resource-definitions.md | 7 +- src/Examples/GettingStarted/Startup.cs | 14 +- .../Repositories/DbContextARepository.cs | 4 +- .../Repositories/DbContextBRepository.cs | 4 +- .../NoEntityFrameworkExample/Startup.cs | 4 +- .../AtomicOperations/LocalIdValidator.cs | 18 +-- .../OperationProcessorAccessor.cs | 9 +- .../AtomicOperations/OperationsProcessor.cs | 11 +- .../Processors/CreateProcessor.cs | 9 +- .../Configuration/IResourceGraph.cs | 41 ++--- .../InverseNavigationResolver.cs | 6 +- .../JsonApiApplicationBuilder.cs | 4 +- .../Configuration/ResourceDescriptor.cs | 10 +- .../Configuration/ResourceGraph.cs | 68 ++++---- .../Configuration/ResourceGraphBuilder.cs | 106 +++++++------ .../Configuration/ResourceNameFormatter.cs | 8 +- .../{ResourceContext.cs => ResourceType.cs} | 43 +++--- .../ServiceCollectionExtensions.cs | 6 +- .../Configuration/ServiceDiscoveryFacade.cs | 10 +- .../Configuration/TypeLocator.cs | 4 +- .../BaseJsonApiOperationsController.cs | 8 +- .../Controllers/JsonApiQueryController.cs | 4 +- .../Controllers/ModelStateViolation.cs | 8 +- .../Errors/InvalidModelStateException.cs | 20 +-- .../Middleware/IControllerResourceMapping.cs | 5 +- .../Middleware/IJsonApiRequest.cs | 8 +- .../Middleware/JsonApiMiddleware.cs | 37 ++--- .../Middleware/JsonApiRequest.cs | 8 +- .../Middleware/JsonApiRoutingConvention.cs | 49 +++--- .../Expressions/QueryExpressionRewriter.cs | 8 +- .../Expressions/SparseFieldTableExpression.cs | 10 +- .../Queries/IQueryLayerComposer.cs | 14 +- .../Queries/Internal/ISparseFieldSetCache.cs | 6 +- .../Queries/Internal/Parsing/FilterParser.cs | 34 ++-- .../Queries/Internal/Parsing/IncludeParser.cs | 15 +- .../Internal/Parsing/PaginationParser.cs | 15 +- .../Internal/Parsing/QueryExpressionParser.cs | 5 +- .../QueryStringParameterScopeParser.cs | 19 ++- .../Parsing/ResourceFieldChainResolver.cs | 129 +++++++--------- .../Queries/Internal/Parsing/SortParser.cs | 17 +- .../Internal/Parsing/SparseFieldSetParser.cs | 17 +- .../Internal/Parsing/SparseFieldTypeParser.cs | 27 ++-- .../Queries/Internal/QueryLayerComposer.cs | 146 +++++++++--------- .../QueryableBuilding/IncludeClauseBuilder.cs | 16 +- .../QueryableBuilding/QueryableBuilder.cs | 20 +-- .../QueryableBuilding/SelectClauseBuilder.cs | 23 ++- .../Queries/Internal/SparseFieldSetCache.cs | 74 ++++----- src/JsonApiDotNetCore/Queries/QueryLayer.cs | 10 +- .../FilterQueryStringParameterReader.cs | 12 +- .../IncludeQueryStringParameterReader.cs | 10 +- .../PaginationQueryStringParameterReader.cs | 16 +- .../Internal/QueryStringParameterReader.cs | 17 +- ...ourceDefinitionQueryableParameterReader.cs | 4 +- .../SortQueryStringParameterReader.cs | 12 +- ...parseFieldSetQueryStringParameterReader.cs | 18 +-- .../Repositories/DbContextExtensions.cs | 8 +- .../Repositories/DbContextResolver.cs | 12 +- .../EntityFrameworkCoreRepository.cs | 23 ++- .../IResourceRepositoryAccessor.cs | 4 +- .../ResourceRepositoryAccessor.cs | 34 ++-- .../Annotations/RelationshipAttribute.cs | 43 ++++-- .../Resources/IResourceDefinitionAccessor.cs | 15 +- .../Resources/IResourceFactory.cs | 4 +- .../Resources/JsonApiResourceDefinition.cs | 4 +- .../Resources/ResourceChangeTracker.cs | 10 +- .../Resources/ResourceDefinitionAccessor.cs | 32 ++-- .../Resources/ResourceFactory.cs | 18 +-- .../JsonConverters/ResourceObjectConverter.cs | 10 +- .../Adapters/AtomicOperationObjectAdapter.cs | 12 +- .../Adapters/AtomicReferenceAdapter.cs | 12 +- .../Request/Adapters/AtomicReferenceResult.cs | 8 +- .../Request/Adapters/DocumentAdapter.cs | 8 +- ...tInResourceOrRelationshipRequestAdapter.cs | 2 +- .../Adapters/IResourceObjectAdapter.cs | 2 +- .../Adapters/RelationshipDataAdapter.cs | 9 +- .../Request/Adapters/ResourceDataAdapter.cs | 4 +- .../ResourceDataInOperationsRequestAdapter.cs | 8 +- .../ResourceIdentifierObjectAdapter.cs | 2 +- .../Adapters/ResourceIdentityAdapter.cs | 34 ++-- .../Adapters/ResourceIdentityRequirements.cs | 2 +- .../Request/Adapters/ResourceObjectAdapter.cs | 53 ++++--- .../Serialization/Response/ILinkBuilder.cs | 3 +- .../Serialization/Response/LinkBuilder.cs | 85 +++++----- .../Response/ResponseModelAdapter.cs | 55 ++++--- .../Services/JsonApiResourceService.cs | 32 ++-- .../ServiceDiscoveryFacadeTests.cs | 12 +- test/DiscoveryTests/TestResourceRepository.cs | 4 +- .../TelevisionBroadcastDefinition.cs | 6 +- .../Transactions/LyricRepository.cs | 8 +- .../Transactions/MusicTrackRepository.cs | 4 +- .../CarCompositeKeyAwareRepository.cs | 8 +- .../CompositeKeys/CarExpressionRewriter.cs | 6 +- .../EagerLoading/BuildingRepository.cs | 4 +- .../QueryStrings/Filtering/FilterTests.cs | 4 +- .../QueryStrings/Includes/IncludeTests.cs | 4 +- .../PaginationWithTotalCountTests.cs | 4 +- .../QueryStrings/Sorting/SortTests.cs | 4 +- .../ResultCapturingRepository.cs | 4 +- .../Reading/MoonDefinition.cs | 2 +- .../Reading/PlanetDefinition.cs | 2 +- .../SoftDeletionAwareResourceService.cs | 12 +- .../UnitTests/Links/LinkInclusionTests.cs | 42 ++--- .../QueryStringParameters/BaseParseTests.cs | 3 +- .../QueryStringParameters/FilterParseTests.cs | 23 +-- .../IncludeParseTests.cs | 4 +- .../LegacyFilterParseTests.cs | 21 +-- .../PaginationParseTests.cs | 16 +- .../QueryStringParameters/SortParseTests.cs | 17 +- .../SparseFieldSetParseTests.cs | 6 +- .../Serialization/InputConversionTests.cs | 8 +- .../IntegrationTestContext.cs | 2 +- .../Builders/ResourceGraphBuilderTests.cs | 25 +-- .../ServiceCollectionExtensionsTests.cs | 8 +- .../ResourceDescriptorAssemblyCacheTests.cs | 12 +- test/UnitTests/Graph/TypeLocatorTests.cs | 12 +- .../Internal/ResourceGraphBuilderTests.cs | 14 +- .../Middleware/JsonApiMiddlewareTests.cs | 63 +++----- .../Middleware/JsonApiRequestTests.cs | 10 +- 126 files changed, 1070 insertions(+), 1149 deletions(-) rename src/JsonApiDotNetCore/Configuration/{ResourceContext.cs => ResourceType.cs} (86%) diff --git a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs index e362b059bd..a99861b562 100644 --- a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs +++ b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs @@ -37,14 +37,14 @@ protected DeserializationBenchmarkBase() var targetedFields = new TargetedFields(); var resourceIdentifierObjectAdapter = new ResourceIdentifierObjectAdapter(resourceGraph, resourceFactory); - var relationshipDataAdapter = new RelationshipDataAdapter(resourceGraph, resourceIdentifierObjectAdapter); + var relationshipDataAdapter = new RelationshipDataAdapter(resourceIdentifierObjectAdapter); var resourceObjectAdapter = new ResourceObjectAdapter(resourceGraph, resourceFactory, options, relationshipDataAdapter); var resourceDataAdapter = new ResourceDataAdapter(resourceDefinitionAccessor, resourceObjectAdapter); var atomicReferenceAdapter = new AtomicReferenceAdapter(resourceGraph, resourceFactory); var atomicOperationResourceDataAdapter = new ResourceDataInOperationsRequestAdapter(resourceDefinitionAccessor, resourceObjectAdapter); - var atomicOperationObjectAdapter = new AtomicOperationObjectAdapter(resourceGraph, options, atomicReferenceAdapter, + var atomicOperationObjectAdapter = new AtomicOperationObjectAdapter(options, atomicReferenceAdapter, atomicOperationResourceDataAdapter, relationshipDataAdapter); var resourceDocumentAdapter = new DocumentInResourceOrRelationshipRequestAdapter(options, resourceDataAdapter, relationshipDataAdapter); diff --git a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs index d603cd744a..d3fe50ffa6 100644 --- a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs @@ -143,7 +143,7 @@ protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGr return new() { Kind = EndpointKind.Primary, - PrimaryResource = resourceGraph.GetResourceContext(), + PrimaryResourceType = resourceGraph.GetResourceType(), WriteOperation = WriteOperationKind.CreateResource }; } diff --git a/benchmarks/Query/QueryParserBenchmarks.cs b/benchmarks/Query/QueryParserBenchmarks.cs index 8f1ec950da..bb6cec20e8 100644 --- a/benchmarks/Query/QueryParserBenchmarks.cs +++ b/benchmarks/Query/QueryParserBenchmarks.cs @@ -36,7 +36,7 @@ public QueryParserBenchmarks() var request = new JsonApiRequest { - PrimaryResource = resourceGraph.GetResourceContext(typeof(BenchmarkResource)), + PrimaryResourceType = resourceGraph.GetResourceType(typeof(BenchmarkResource)), IsCollection = true }; diff --git a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs index 49f25622c5..fbcdf0b0a9 100644 --- a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs +++ b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs @@ -122,10 +122,10 @@ public string SerializeOperationsResponse() protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) { - return new JsonApiRequest + return new() { Kind = EndpointKind.AtomicOperations, - PrimaryResource = resourceGraph.GetResourceContext() + PrimaryResourceType = resourceGraph.GetResourceType() }; } diff --git a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs index 5237ad2642..8f538cc9a2 100644 --- a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs +++ b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs @@ -113,21 +113,21 @@ public string SerializeResourceResponse() protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) { - return new JsonApiRequest + return new() { Kind = EndpointKind.Primary, - PrimaryResource = resourceGraph.GetResourceContext() + PrimaryResourceType = resourceGraph.GetResourceType() }; } protected override IEvaluatedIncludeCache CreateEvaluatedIncludeCache(IResourceGraph resourceGraph) { - ResourceContext resourceContext = resourceGraph.GetResourceContext(); + ResourceType resourceAType = resourceGraph.GetResourceType(); - RelationshipAttribute single2 = resourceContext.GetRelationshipByPropertyName(nameof(ResourceA.Single2)); - RelationshipAttribute single3 = resourceContext.GetRelationshipByPropertyName(nameof(ResourceA.Single3)); - RelationshipAttribute multi4 = resourceContext.GetRelationshipByPropertyName(nameof(ResourceA.Multi4)); - RelationshipAttribute multi5 = resourceContext.GetRelationshipByPropertyName(nameof(ResourceA.Multi5)); + RelationshipAttribute single2 = resourceAType.GetRelationshipByPropertyName(nameof(ResourceA.Single2)); + RelationshipAttribute single3 = resourceAType.GetRelationshipByPropertyName(nameof(ResourceA.Single3)); + RelationshipAttribute multi4 = resourceAType.GetRelationshipByPropertyName(nameof(ResourceA.Multi4)); + RelationshipAttribute multi5 = resourceAType.GetRelationshipByPropertyName(nameof(ResourceA.Multi5)); ImmutableArray chain = ArrayFactory.Create(single2, single3, multi4, multi5).ToImmutableArray(); IEnumerable chains = new ResourceFieldChainExpression(chain).AsEnumerable(); diff --git a/benchmarks/Serialization/SerializationBenchmarkBase.cs b/benchmarks/Serialization/SerializationBenchmarkBase.cs index f7de226ed2..716169423b 100644 --- a/benchmarks/Serialization/SerializationBenchmarkBase.cs +++ b/benchmarks/Serialization/SerializationBenchmarkBase.cs @@ -55,8 +55,8 @@ protected SerializationBenchmarkBase() var sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); var requestQueryStringAccessor = new FakeRequestQueryStringAccessor(); - ResponseModelAdapter = new ResponseModelAdapter(request, options, ResourceGraph, linkBuilder, metaBuilder, resourceDefinitionAccessor, - evaluatedIncludeCache, sparseFieldSetCache, requestQueryStringAccessor); + ResponseModelAdapter = new ResponseModelAdapter(request, options, linkBuilder, metaBuilder, resourceDefinitionAccessor, evaluatedIncludeCache, + sparseFieldSetCache, requestQueryStringAccessor); } protected abstract JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph); @@ -129,37 +129,37 @@ public sealed class ResourceA : Identifiable private sealed class FakeResourceDefinitionAccessor : IResourceDefinitionAccessor { - public IImmutableSet OnApplyIncludes(Type resourceType, IImmutableSet existingIncludes) + public IImmutableSet OnApplyIncludes(ResourceType resourceType, IImmutableSet existingIncludes) { return existingIncludes; } - public FilterExpression OnApplyFilter(Type resourceType, FilterExpression existingFilter) + public FilterExpression OnApplyFilter(ResourceType resourceType, FilterExpression existingFilter) { return existingFilter; } - public SortExpression OnApplySort(Type resourceType, SortExpression existingSort) + public SortExpression OnApplySort(ResourceType resourceType, SortExpression existingSort) { return existingSort; } - public PaginationExpression OnApplyPagination(Type resourceType, PaginationExpression existingPagination) + public PaginationExpression OnApplyPagination(ResourceType resourceType, PaginationExpression existingPagination) { return existingPagination; } - public SparseFieldSetExpression OnApplySparseFieldSet(Type resourceType, SparseFieldSetExpression existingSparseFieldSet) + public SparseFieldSetExpression OnApplySparseFieldSet(ResourceType resourceType, SparseFieldSetExpression existingSparseFieldSet) { return existingSparseFieldSet; } - public object GetQueryableHandlerForQueryStringParameter(Type resourceType, string parameterName) + public object GetQueryableHandlerForQueryStringParameter(Type resourceClrType, string parameterName) { return null; } - public IDictionary GetMeta(Type resourceType, IIdentifiable resourceInstance) + public IDictionary GetMeta(ResourceType resourceType, IIdentifiable resourceInstance) { return null; } @@ -223,15 +223,15 @@ private sealed class FakeLinkBuilder : ILinkBuilder { public TopLevelLinks GetTopLevelLinks() { - return new TopLevelLinks + return new() { Self = "TopLevel:Self" }; } - public ResourceLinks GetResourceLinks(string resourceName, string id) + public ResourceLinks GetResourceLinks(ResourceType resourceType, string id) { - return new ResourceLinks + return new() { Self = "Resource:Self" }; @@ -239,7 +239,7 @@ public ResourceLinks GetResourceLinks(string resourceName, string id) public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable leftResource) { - return new RelationshipLinks + return new() { Self = "Relationship:Self", Related = "Relationship:Related" diff --git a/docs/getting-started/step-by-step.md b/docs/getting-started/step-by-step.md index 9273de6eb1..1443409b7f 100644 --- a/docs/getting-started/step-by-step.md +++ b/docs/getting-started/step-by-step.md @@ -114,18 +114,18 @@ public void Configure(IApplicationBuilder app) One way to seed the database is in your Configure method: ```c# -public void Configure(IApplicationBuilder app, AppDbContext context) +public void Configure(IApplicationBuilder app, AppDbContext dbContext) { - context.Database.EnsureCreated(); + dbContext.Database.EnsureCreated(); - if (!context.People.Any()) + if (!dbContext.People.Any()) { - context.People.Add(new Person + dbContext.People.Add(new Person { Name = "John Doe" }); - context.SaveChanges(); + dbContext.SaveChanges(); } app.UseRouting(); diff --git a/docs/usage/extensibility/repositories.md b/docs/usage/extensibility/repositories.md index 623c959510..1ddd025ac5 100644 --- a/docs/usage/extensibility/repositories.md +++ b/docs/usage/extensibility/repositories.md @@ -39,12 +39,12 @@ public class ArticleRepository : EntityFrameworkCoreRepository
private readonly IAuthenticationService _authenticationService; public ArticleRepository(IAuthenticationService authenticationService, - ITargetedFields targetedFields, IDbContextResolver contextResolver, + ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, + : base(targetedFields, dbContextResolver, resourceGraph, genericServiceFactory, resourceFactory, constraintProviders, loggerFactory) { _authenticationService = authenticationService; @@ -68,13 +68,13 @@ public class DbContextARepository : EntityFrameworkCoreRepository { public DbContextARepository(ITargetedFields targetedFields, - DbContextResolver contextResolver, + DbContextResolver dbContextResolver, // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, + : base(targetedFields, dbContextResolver, resourceGraph, genericServiceFactory, resourceFactory, constraintProviders, loggerFactory) { } diff --git a/docs/usage/extensibility/resource-definitions.md b/docs/usage/extensibility/resource-definitions.md index 5f0ca406be..6ebace8d52 100644 --- a/docs/usage/extensibility/resource-definitions.md +++ b/docs/usage/extensibility/resource-definitions.md @@ -172,11 +172,8 @@ public class AccountDefinition : JsonApiResourceDefinition public override FilterExpression OnApplyFilter(FilterExpression existingFilter) { - var resourceContext = ResourceGraph.GetResourceContext(); - - var isSuspendedAttribute = - resourceContext.Attributes.Single(account => - account.Property.Name == nameof(Account.IsSuspended)); + var isSuspendedAttribute = ResourceType.Attributes.Single(account => + account.Property.Name == nameof(Account.IsSuspended)); var isNotSuspended = new ComparisonExpression(ComparisonOperator.Equals, new ResourceFieldChainExpression(isSuspendedAttribute), diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs index 10d2f338f0..13beab63fe 100644 --- a/src/Examples/GettingStarted/Startup.cs +++ b/src/Examples/GettingStarted/Startup.cs @@ -26,22 +26,22 @@ public void ConfigureServices(IServiceCollection services) // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. [UsedImplicitly] - public void Configure(IApplicationBuilder app, SampleDbContext context) + public void Configure(IApplicationBuilder app, SampleDbContext dbContext) { - context.Database.EnsureDeleted(); - context.Database.EnsureCreated(); - CreateSampleData(context); + dbContext.Database.EnsureDeleted(); + dbContext.Database.EnsureCreated(); + CreateSampleData(dbContext); app.UseRouting(); app.UseJsonApi(); app.UseEndpoints(endpoints => endpoints.MapControllers()); } - private static void CreateSampleData(SampleDbContext context) + private static void CreateSampleData(SampleDbContext dbContext) { // Note: The generate-examples.ps1 script (to create example requests in documentation) depends on these. - context.Books.AddRange(new Book + dbContext.Books.AddRange(new Book { Title = "Frankenstein", PublishYear = 1818, @@ -67,7 +67,7 @@ private static void CreateSampleData(SampleDbContext context) } }); - context.SaveChanges(); + dbContext.SaveChanges(); } } } diff --git a/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs b/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs index 5b07948005..fc5d58efd9 100644 --- a/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs +++ b/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs @@ -13,10 +13,10 @@ namespace MultiDbContextExample.Repositories public sealed class DbContextARepository : EntityFrameworkCoreRepository where TResource : class, IIdentifiable { - public DbContextARepository(ITargetedFields targetedFields, DbContextResolver contextResolver, IResourceGraph resourceGraph, + public DbContextARepository(ITargetedFields targetedFields, DbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) + : base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } } diff --git a/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs b/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs index afa7ed4bde..7c1ef16ec7 100644 --- a/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs +++ b/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs @@ -13,10 +13,10 @@ namespace MultiDbContextExample.Repositories public sealed class DbContextBRepository : EntityFrameworkCoreRepository where TResource : class, IIdentifiable { - public DbContextBRepository(ITargetedFields targetedFields, DbContextResolver contextResolver, IResourceGraph resourceGraph, + public DbContextBRepository(ITargetedFields targetedFields, DbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) + : base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } } diff --git a/src/Examples/NoEntityFrameworkExample/Startup.cs b/src/Examples/NoEntityFrameworkExample/Startup.cs index c51985f5f2..758c31f731 100644 --- a/src/Examples/NoEntityFrameworkExample/Startup.cs +++ b/src/Examples/NoEntityFrameworkExample/Startup.cs @@ -34,9 +34,9 @@ public void ConfigureServices(IServiceCollection services) // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. [UsedImplicitly] - public void Configure(IApplicationBuilder app, AppDbContext context) + public void Configure(IApplicationBuilder app, AppDbContext dbContext) { - context.Database.EnsureCreated(); + dbContext.Database.EnsureCreated(); app.UseRouting(); app.UseJsonApi(); diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs index 736c91ea98..5fd790a318 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs @@ -59,7 +59,7 @@ private void ValidateOperation(OperationContainer operation) { if (operation.Request.WriteOperation == WriteOperationKind.CreateResource) { - DeclareLocalId(operation.Resource); + DeclareLocalId(operation.Resource, operation.Request.PrimaryResourceType); } else { @@ -73,25 +73,23 @@ private void ValidateOperation(OperationContainer operation) if (operation.Request.WriteOperation == WriteOperationKind.CreateResource) { - AssignLocalId(operation); + AssignLocalId(operation, operation.Request.PrimaryResourceType); } } - private void DeclareLocalId(IIdentifiable resource) + private void DeclareLocalId(IIdentifiable resource, ResourceType resourceType) { if (resource.LocalId != null) { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resource.GetType()); - _localIdTracker.Declare(resource.LocalId, resourceContext.PublicName); + _localIdTracker.Declare(resource.LocalId, resourceType.PublicName); } } - private void AssignLocalId(OperationContainer operation) + private void AssignLocalId(OperationContainer operation, ResourceType resourceType) { if (operation.Resource.LocalId != null) { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(operation.Resource.GetType()); - _localIdTracker.Assign(operation.Resource.LocalId, resourceContext.PublicName, "placeholder"); + _localIdTracker.Assign(operation.Resource.LocalId, resourceType.PublicName, "placeholder"); } } @@ -99,8 +97,8 @@ private void AssertLocalIdIsAssigned(IIdentifiable resource) { if (resource.LocalId != null) { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resource.GetType()); - _localIdTracker.GetValue(resource.LocalId, resourceContext.PublicName); + ResourceType resourceType = _resourceGraph.GetResourceType(resource.GetType()); + _localIdTracker.GetValue(resource.LocalId, resourceType.PublicName); } } } diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs index 516d40b90c..68cfb752b2 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs @@ -14,15 +14,12 @@ namespace JsonApiDotNetCore.AtomicOperations [PublicAPI] public class OperationProcessorAccessor : IOperationProcessorAccessor { - private readonly IResourceGraph _resourceGraph; private readonly IServiceProvider _serviceProvider; - public OperationProcessorAccessor(IResourceGraph resourceGraph, IServiceProvider serviceProvider) + public OperationProcessorAccessor(IServiceProvider serviceProvider) { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); - _resourceGraph = resourceGraph; _serviceProvider = serviceProvider; } @@ -38,9 +35,9 @@ public Task ProcessAsync(OperationContainer operation, Cance protected virtual IOperationProcessor ResolveProcessor(OperationContainer operation) { Type processorInterface = GetProcessorInterface(operation.Request.WriteOperation.GetValueOrDefault()); - ResourceContext resourceContext = _resourceGraph.GetResourceContext(operation.Resource.GetType()); + ResourceType resourceType = operation.Request.PrimaryResourceType; - Type processorType = processorInterface.MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); + Type processorType = processorInterface.MakeGenericType(resourceType.ClrType, resourceType.IdentityClrType); return (IOperationProcessor)_serviceProvider.GetRequiredService(processorType); } diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs index 6338aa40d0..be266286db 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs @@ -119,7 +119,7 @@ protected void TrackLocalIdsForOperation(OperationContainer operation) { if (operation.Request.WriteOperation == WriteOperationKind.CreateResource) { - DeclareLocalId(operation.Resource); + DeclareLocalId(operation.Resource, operation.Request.PrimaryResourceType); } else { @@ -132,12 +132,11 @@ protected void TrackLocalIdsForOperation(OperationContainer operation) } } - private void DeclareLocalId(IIdentifiable resource) + private void DeclareLocalId(IIdentifiable resource, ResourceType resourceType) { if (resource.LocalId != null) { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resource.GetType()); - _localIdTracker.Declare(resource.LocalId, resourceContext.PublicName); + _localIdTracker.Declare(resource.LocalId, resourceType.PublicName); } } @@ -145,8 +144,8 @@ private void AssignStringId(IIdentifiable resource) { if (resource.LocalId != null) { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resource.GetType()); - resource.StringId = _localIdTracker.GetValue(resource.LocalId, resourceContext.PublicName); + ResourceType resourceType = _resourceGraph.GetResourceType(resource.GetType()); + resource.StringId = _localIdTracker.GetValue(resource.LocalId, resourceType.PublicName); } } } diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/CreateProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/CreateProcessor.cs index 2e113561ab..4a408f368e 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/CreateProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/CreateProcessor.cs @@ -14,17 +14,14 @@ public class CreateProcessor : ICreateProcessor { private readonly ICreateService _service; private readonly ILocalIdTracker _localIdTracker; - private readonly IResourceGraph _resourceGraph; - public CreateProcessor(ICreateService service, ILocalIdTracker localIdTracker, IResourceGraph resourceGraph) + public CreateProcessor(ICreateService service, ILocalIdTracker localIdTracker) { ArgumentGuard.NotNull(service, nameof(service)); ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker)); - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _service = service; _localIdTracker = localIdTracker; - _resourceGraph = resourceGraph; } /// @@ -37,9 +34,9 @@ public virtual async Task ProcessAsync(OperationContainer op if (operation.Resource.LocalId != null) { string serverId = newResource != null ? newResource.StringId : operation.Resource.StringId; - ResourceContext resourceContext = _resourceGraph.GetResourceContext(); + ResourceType resourceType = operation.Request.PrimaryResourceType; - _localIdTracker.Assign(operation.Resource.LocalId, resourceContext.PublicName, serverId); + _localIdTracker.Assign(operation.Resource.LocalId, resourceType.PublicName, serverId); } return newResource == null ? null : operation.WithResource(newResource); diff --git a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs index b7216f0f8c..1c2c058150 100644 --- a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs @@ -17,43 +17,44 @@ public interface IResourceGraph /// /// Gets the metadata for all registered resources. /// - IReadOnlySet GetResourceContexts(); + IReadOnlySet GetResourceTypes(); /// - /// Gets the resource metadata for the resource that is publicly exposed by the specified name. Throws an when - /// not found. + /// Gets the metadata for the resource that is publicly exposed by the specified name. Throws an when not found. /// - ResourceContext GetResourceContext(string publicName); + ResourceType GetResourceType(string publicName); /// - /// Gets the resource metadata for the specified resource type. Throws an when not found. + /// Gets the metadata for the resource of the specified CLR type. Throws an when not found. /// - ResourceContext GetResourceContext(Type resourceType); + ResourceType GetResourceType(Type resourceClrType); /// - /// Gets the resource metadata for the specified resource type. Throws an when not found. + /// Gets the metadata for the resource of the specified CLR type. Throws an when not found. /// - ResourceContext GetResourceContext() + ResourceType GetResourceType() 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. + /// Attempts to get the metadata for the resource that is publicly exposed by the specified name. Returns null when not found. /// - ResourceContext TryGetResourceContext(string publicName); + ResourceType TryGetResourceType(string publicName); /// - /// Attempts to get the resource metadata for the specified resource type. Returns null when not found. + /// Attempts to get metadata for the resource of the specified CLR type. Returns null when not found. /// - ResourceContext TryGetResourceContext(Type resourceType); + ResourceType TryGetResourceType(Type resourceClrType); /// /// Gets the fields (attributes and relationships) for that are targeted by the selector. /// /// - /// The resource type for which to retrieve fields. + /// The resource CLR type for which to retrieve fields. /// /// - /// Should be of the form: (TResource r) => new { r.Field1, r.Field2 } + /// Should be of the form: new { resource.Attribute1, resource.Relationship2 } + /// ]]> /// IReadOnlyCollection GetFields(Expression> selector) where TResource : class, IIdentifiable; @@ -62,10 +63,12 @@ IReadOnlyCollection GetFields(Expression that are targeted by the selector. ///
/// - /// The resource type for which to retrieve attributes. + /// The resource CLR type for which to retrieve attributes. /// /// - /// Should be of the form: (TResource r) => new { r.Attribute1, r.Attribute2 } + /// Should be of the form: new { resource.attribute1, resource.Attribute2 } + /// ]]> /// IReadOnlyCollection GetAttributes(Expression> selector) where TResource : class, IIdentifiable; @@ -74,10 +77,12 @@ IReadOnlyCollection GetAttributes(Expression that are targeted by the selector. ///
/// - /// The resource type for which to retrieve relationships. + /// The resource CLR type for which to retrieve relationships. /// /// - /// Should be of the form: (TResource r) => new { r.Relationship1, r.Relationship2 } + /// Should be of the form: new { resource.Relationship1, resource.Relationship2 } + /// ]]> /// IReadOnlyCollection GetRelationships(Expression> selector) where TResource : class, IIdentifiable; diff --git a/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs b/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs index abe95d00bf..a3461cfdcf 100644 --- a/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs +++ b/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs @@ -36,14 +36,14 @@ public void Resolve() private void Resolve(DbContext dbContext) { - foreach (ResourceContext resourceContext in _resourceGraph.GetResourceContexts().Where(context => context.Relationships.Any())) + foreach (ResourceType resourceType in _resourceGraph.GetResourceTypes().Where(resourceType => resourceType.Relationships.Any())) { - IEntityType entityType = dbContext.Model.FindEntityType(resourceContext.ResourceType); + IEntityType entityType = dbContext.Model.FindEntityType(resourceType.ClrType); if (entityType != null) { IDictionary navigationMap = GetNavigations(entityType); - ResolveRelationships(resourceContext.Relationships, navigationMap); + ResolveRelationships(resourceType.Relationships, navigationMap); } } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index d911290ace..2a128c6266 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -135,8 +135,8 @@ public void ConfigureServiceContainer(ICollection dbContextTypes) foreach (Type dbContextType in dbContextTypes) { - Type contextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); - _services.AddScoped(typeof(IDbContextResolver), contextResolverType); + Type dbContextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); + _services.AddScoped(typeof(IDbContextResolver), dbContextResolverType); } _services.AddScoped(); diff --git a/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs b/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs index 70a14513ae..d673fe11a7 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs @@ -4,13 +4,13 @@ namespace JsonApiDotNetCore.Configuration { internal sealed class ResourceDescriptor { - public Type ResourceType { get; } - public Type IdType { get; } + public Type ResourceClrType { get; } + public Type IdClrType { get; } - public ResourceDescriptor(Type resourceType, Type idType) + public ResourceDescriptor(Type resourceClrType, Type idClrType) { - ResourceType = resourceType; - IdType = idType; + ResourceClrType = resourceClrType; + IdClrType = idClrType; } } } diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs index 48654ea0a8..90df89c576 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs @@ -15,82 +15,82 @@ public sealed class ResourceGraph : IResourceGraph { private static readonly Type ProxyTargetAccessorType = Type.GetType("Castle.DynamicProxy.IProxyTargetAccessor, Castle.Core"); - private readonly IReadOnlySet _resourceContextSet; - private readonly Dictionary _resourceContextsByType = new(); - private readonly Dictionary _resourceContextsByPublicName = new(); + private readonly IReadOnlySet _resourceTypeSet; + private readonly Dictionary _resourceTypesByClrType = new(); + private readonly Dictionary _resourceTypesByPublicName = new(); - public ResourceGraph(IReadOnlySet resourceContexts) + public ResourceGraph(IReadOnlySet resourceTypeSet) { - ArgumentGuard.NotNull(resourceContexts, nameof(resourceContexts)); + ArgumentGuard.NotNull(resourceTypeSet, nameof(resourceTypeSet)); - _resourceContextSet = resourceContexts; + _resourceTypeSet = resourceTypeSet; - foreach (ResourceContext resourceContext in resourceContexts) + foreach (ResourceType resourceType in resourceTypeSet) { - _resourceContextsByType.Add(resourceContext.ResourceType, resourceContext); - _resourceContextsByPublicName.Add(resourceContext.PublicName, resourceContext); + _resourceTypesByClrType.Add(resourceType.ClrType, resourceType); + _resourceTypesByPublicName.Add(resourceType.PublicName, resourceType); } } /// - public IReadOnlySet GetResourceContexts() + public IReadOnlySet GetResourceTypes() { - return _resourceContextSet; + return _resourceTypeSet; } /// - public ResourceContext GetResourceContext(string publicName) + public ResourceType GetResourceType(string publicName) { - ResourceContext resourceContext = TryGetResourceContext(publicName); + ResourceType resourceType = TryGetResourceType(publicName); - if (resourceContext == null) + if (resourceType == null) { throw new InvalidOperationException($"Resource type '{publicName}' does not exist."); } - return resourceContext; + return resourceType; } /// - public ResourceContext TryGetResourceContext(string publicName) + public ResourceType TryGetResourceType(string publicName) { ArgumentGuard.NotNull(publicName, nameof(publicName)); - return _resourceContextsByPublicName.TryGetValue(publicName, out ResourceContext resourceContext) ? resourceContext : null; + return _resourceTypesByPublicName.TryGetValue(publicName, out ResourceType resourceType) ? resourceType : null; } /// - public ResourceContext GetResourceContext(Type resourceType) + public ResourceType GetResourceType(Type resourceClrType) { - ResourceContext resourceContext = TryGetResourceContext(resourceType); + ResourceType resourceType = TryGetResourceType(resourceClrType); - if (resourceContext == null) + if (resourceType == null) { - throw new InvalidOperationException($"Resource of type '{resourceType.Name}' does not exist."); + throw new InvalidOperationException($"Resource of type '{resourceClrType.Name}' does not exist."); } - return resourceContext; + return resourceType; } /// - public ResourceContext TryGetResourceContext(Type resourceType) + public ResourceType TryGetResourceType(Type resourceClrType) { - ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(resourceClrType, nameof(resourceClrType)); - Type typeToFind = IsLazyLoadingProxyForResourceType(resourceType) ? resourceType.BaseType : resourceType; - return _resourceContextsByType.TryGetValue(typeToFind!, out ResourceContext resourceContext) ? resourceContext : null; + Type typeToFind = IsLazyLoadingProxyForResourceType(resourceClrType) ? resourceClrType.BaseType : resourceClrType; + return _resourceTypesByClrType.TryGetValue(typeToFind!, out ResourceType resourceType) ? resourceType : null; } - private bool IsLazyLoadingProxyForResourceType(Type resourceType) + private bool IsLazyLoadingProxyForResourceType(Type resourceClrType) { - return ProxyTargetAccessorType?.IsAssignableFrom(resourceType) ?? false; + return ProxyTargetAccessorType?.IsAssignableFrom(resourceClrType) ?? false; } /// - public ResourceContext GetResourceContext() + public ResourceType GetResourceType() where TResource : class, IIdentifiable { - return GetResourceContext(typeof(TResource)); + return GetResourceType(typeof(TResource)); } /// @@ -145,19 +145,19 @@ private IReadOnlyCollection FilterFields(Expression GetFieldsOfType() where TKind : ResourceFieldAttribute { - ResourceContext resourceContext = GetResourceContext(typeof(TResource)); + ResourceType resourceType = GetResourceType(typeof(TResource)); if (typeof(TKind) == typeof(AttrAttribute)) { - return (IReadOnlyCollection)resourceContext.Attributes; + return (IReadOnlyCollection)resourceType.Attributes; } if (typeof(TKind) == typeof(RelationshipAttribute)) { - return (IReadOnlyCollection)resourceContext.Relationships; + return (IReadOnlyCollection)resourceType.Relationships; } - return (IReadOnlyCollection)resourceContext.Fields; + return (IReadOnlyCollection)resourceType.Fields; } private IEnumerable ToMemberNames(Expression> selector) diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 0ea1fb1a14..c4e29e9ae2 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -17,7 +17,7 @@ public class ResourceGraphBuilder { private readonly IJsonApiOptions _options; private readonly ILogger _logger; - private readonly HashSet _resourceContexts = new(); + private readonly HashSet _resourceTypes = new(); private readonly TypeLocator _typeLocator = new(); public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactory) @@ -34,18 +34,26 @@ public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactor ///
public IResourceGraph Build() { - return new ResourceGraph(_resourceContexts); + var resourceGraph = new ResourceGraph(_resourceTypes); + + foreach (RelationshipAttribute relationship in _resourceTypes.SelectMany(resourceType => resourceType.Relationships)) + { + relationship.LeftType = resourceGraph.GetResourceType(relationship.LeftClrType); + relationship.RightType = resourceGraph.GetResourceType(relationship.RightClrType); + } + + return resourceGraph; } /// - /// Adds a JSON:API resource with int as the identifier type. + /// Adds a JSON:API resource with int as the identifier CLR type. /// /// - /// The resource model type. + /// The resource CLR type. /// /// - /// The name under which the resource is publicly exposed by the API. If nothing is specified, the configured naming convention formatter will be - /// applied. + /// The name under which the resource is publicly exposed by the API. If nothing is specified, the naming convention is applied on the pluralized CLR + /// type name. /// public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable @@ -57,14 +65,14 @@ public ResourceGraphBuilder Add(string publicName = null) /// Adds a JSON:API resource. ///
/// - /// The resource model type. + /// The resource CLR type. /// /// - /// The resource model identifier type. + /// The resource identifier CLR type. /// /// - /// The name under which the resource is publicly exposed by the API. If nothing is specified, the configured naming convention formatter will be - /// applied. + /// The name under which the resource is publicly exposed by the API. If nothing is specified, the naming convention is applied on the pluralized CLR + /// type name. /// public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable @@ -75,60 +83,60 @@ public ResourceGraphBuilder Add(string publicName = null) /// /// Adds a JSON:API resource. /// - /// - /// The resource model type. + /// + /// The resource CLR type. /// - /// - /// The resource model identifier type. + /// + /// The resource identifier CLR type. /// /// - /// The name under which the resource is publicly exposed by the API. If nothing is specified, the configured naming convention formatter will be - /// applied. + /// The name under which the resource is publicly exposed by the API. If nothing is specified, the naming convention is applied on the pluralized CLR + /// type name. /// - public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string publicName = null) + public ResourceGraphBuilder Add(Type resourceClrType, Type idClrType = null, string publicName = null) { - ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(resourceClrType, nameof(resourceClrType)); - if (_resourceContexts.Any(resourceContext => resourceContext.ResourceType == resourceType)) + if (_resourceTypes.Any(resourceType => resourceType.ClrType == resourceClrType)) { return this; } - if (resourceType.IsOrImplementsInterface(typeof(IIdentifiable))) + if (resourceClrType.IsOrImplementsInterface(typeof(IIdentifiable))) { - string effectivePublicName = publicName ?? FormatResourceName(resourceType); - Type effectiveIdType = idType ?? _typeLocator.TryGetIdType(resourceType); + string effectivePublicName = publicName ?? FormatResourceName(resourceClrType); + Type effectiveIdType = idClrType ?? _typeLocator.TryGetIdType(resourceClrType) ?? typeof(int); - ResourceContext resourceContext = CreateResourceContext(effectivePublicName, resourceType, effectiveIdType); - _resourceContexts.Add(resourceContext); + ResourceType resourceType = CreateResourceType(effectivePublicName, resourceClrType, effectiveIdType); + _resourceTypes.Add(resourceType); } else { - _logger.LogWarning($"Entity '{resourceType}' does not implement '{nameof(IIdentifiable)}'."); + _logger.LogWarning($"Entity '{resourceClrType}' does not implement '{nameof(IIdentifiable)}'."); } return this; } - private ResourceContext CreateResourceContext(string publicName, Type resourceType, Type idType) + private ResourceType CreateResourceType(string publicName, Type resourceClrType, Type idClrType) { - IReadOnlyCollection attributes = GetAttributes(resourceType); - IReadOnlyCollection relationships = GetRelationships(resourceType); - IReadOnlyCollection eagerLoads = GetEagerLoads(resourceType); + IReadOnlyCollection attributes = GetAttributes(resourceClrType); + IReadOnlyCollection relationships = GetRelationships(resourceClrType); + IReadOnlyCollection eagerLoads = GetEagerLoads(resourceClrType); - var linksAttribute = (ResourceLinksAttribute)resourceType.GetCustomAttribute(typeof(ResourceLinksAttribute)); + var linksAttribute = (ResourceLinksAttribute)resourceClrType.GetCustomAttribute(typeof(ResourceLinksAttribute)); return linksAttribute == null - ? new ResourceContext(publicName, resourceType, idType, attributes, relationships, eagerLoads) - : new ResourceContext(publicName, resourceType, idType, attributes, relationships, eagerLoads, linksAttribute.TopLevelLinks, + ? new ResourceType(publicName, resourceClrType, idClrType, attributes, relationships, eagerLoads) + : new ResourceType(publicName, resourceClrType, idClrType, attributes, relationships, eagerLoads, linksAttribute.TopLevelLinks, linksAttribute.ResourceLinks, linksAttribute.RelationshipLinks); } - private IReadOnlyCollection GetAttributes(Type resourceType) + private IReadOnlyCollection GetAttributes(Type resourceClrType) { var attributes = new List(); - foreach (PropertyInfo property in resourceType.GetProperties()) + foreach (PropertyInfo property in resourceClrType.GetProperties()) { var attribute = (AttrAttribute)property.GetCustomAttribute(typeof(AttrAttribute)); @@ -167,27 +175,27 @@ private IReadOnlyCollection GetAttributes(Type resourceType) return attributes; } - private IReadOnlyCollection GetRelationships(Type resourceType) + private IReadOnlyCollection GetRelationships(Type resourceClrType) { - var attributes = new List(); - PropertyInfo[] properties = resourceType.GetProperties(); + var relationships = new List(); + PropertyInfo[] properties = resourceClrType.GetProperties(); foreach (PropertyInfo property in properties) { - var attribute = (RelationshipAttribute)property.GetCustomAttribute(typeof(RelationshipAttribute)); + var relationship = (RelationshipAttribute)property.GetCustomAttribute(typeof(RelationshipAttribute)); - if (attribute != null) + if (relationship != null) { - attribute.Property = property; - attribute.PublicName ??= FormatPropertyName(property); - attribute.LeftType = resourceType; - attribute.RightType = GetRelationshipType(attribute, property); + relationship.Property = property; + relationship.PublicName ??= FormatPropertyName(property); + relationship.LeftClrType = resourceClrType; + relationship.RightClrType = GetRelationshipType(relationship, property); - attributes.Add(attribute); + relationships.Add(relationship); } } - return attributes; + return relationships; } private Type GetRelationshipType(RelationshipAttribute relationship, PropertyInfo property) @@ -198,12 +206,12 @@ private Type GetRelationshipType(RelationshipAttribute relationship, PropertyInf return relationship is HasOneAttribute ? property.PropertyType : property.PropertyType.GetGenericArguments()[0]; } - private IReadOnlyCollection GetEagerLoads(Type resourceType, int recursionDepth = 0) + private IReadOnlyCollection GetEagerLoads(Type resourceClrType, int recursionDepth = 0) { AssertNoInfiniteRecursion(recursionDepth); var attributes = new List(); - PropertyInfo[] properties = resourceType.GetProperties(); + PropertyInfo[] properties = resourceClrType.GetProperties(); foreach (PropertyInfo property in properties) { @@ -241,10 +249,10 @@ private Type TypeOrElementType(Type type) return interfaces.Length == 1 ? interfaces.Single().GenericTypeArguments[0] : type; } - private string FormatResourceName(Type resourceType) + private string FormatResourceName(Type resourceClrType) { var formatter = new ResourceNameFormatter(_options.SerializerOptions.PropertyNamingPolicy); - return formatter.FormatResourceName(resourceType); + return formatter.FormatResourceName(resourceClrType); } private string FormatPropertyName(PropertyInfo resourceProperty) diff --git a/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs b/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs index 93e5dda4ff..d968c8eda7 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs @@ -16,16 +16,16 @@ public ResourceNameFormatter(JsonNamingPolicy namingPolicy) } /// - /// Gets the publicly visible resource name for the internal type name using the configured naming convention. + /// Gets the publicly exposed resource name by applying the configured naming convention on the pluralized CLR type name. /// - public string FormatResourceName(Type resourceType) + public string FormatResourceName(Type resourceClrType) { - if (resourceType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) + if (resourceClrType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) { return attribute.PublicName; } - string publicName = resourceType.Name.Pluralize(); + string publicName = resourceClrType.Name.Pluralize(); return _namingPolicy != null ? _namingPolicy.ConvertName(publicName) : publicName; } } diff --git a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs b/src/JsonApiDotNetCore/Configuration/ResourceType.cs similarity index 86% rename from src/JsonApiDotNetCore/Configuration/ResourceContext.cs rename to src/JsonApiDotNetCore/Configuration/ResourceType.cs index f5b42e5499..75dddfbcc4 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceType.cs @@ -10,7 +10,7 @@ namespace JsonApiDotNetCore.Configuration /// Metadata about the shape of a JSON:API resource in the resource graph. ///
[PublicAPI] - public sealed class ResourceContext + public sealed class ResourceType { private readonly Dictionary _fieldsByPublicName = new(); private readonly Dictionary _fieldsByPropertyName = new(); @@ -23,12 +23,12 @@ public sealed class ResourceContext /// /// The CLR type of the resource. /// - public Type ResourceType { get; } + public Type ClrType { get; } /// - /// The identity type of the resource. + /// The CLR type of the resource identity. /// - public Type IdentityType { get; } + public Type IdentityClrType { get; } /// /// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields. @@ -78,28 +78,25 @@ public sealed class ResourceContext /// public LinkTypes RelationshipLinks { get; } - public ResourceContext(string publicName, Type resourceType, Type identityType, IReadOnlyCollection attributes, - IReadOnlyCollection relationships, IReadOnlyCollection eagerLoads, + public ResourceType(string publicName, Type clrType, Type identityClrType, IReadOnlyCollection attributes = null, + IReadOnlyCollection relationships = null, IReadOnlyCollection eagerLoads = null, LinkTypes topLevelLinks = LinkTypes.NotConfigured, LinkTypes resourceLinks = LinkTypes.NotConfigured, LinkTypes relationshipLinks = LinkTypes.NotConfigured) { ArgumentGuard.NotNullNorEmpty(publicName, nameof(publicName)); - ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - ArgumentGuard.NotNull(identityType, nameof(identityType)); - ArgumentGuard.NotNull(attributes, nameof(attributes)); - ArgumentGuard.NotNull(relationships, nameof(relationships)); - ArgumentGuard.NotNull(eagerLoads, nameof(eagerLoads)); + ArgumentGuard.NotNull(clrType, nameof(clrType)); + ArgumentGuard.NotNull(identityClrType, nameof(identityClrType)); PublicName = publicName; - ResourceType = resourceType; - IdentityType = identityType; - Fields = attributes.Cast().Concat(relationships).ToArray(); - Attributes = attributes; - Relationships = relationships; - EagerLoads = eagerLoads; + ClrType = clrType; + IdentityClrType = identityClrType; + Attributes = attributes ?? Array.Empty(); + Relationships = relationships ?? Array.Empty(); + EagerLoads = eagerLoads ?? Array.Empty(); TopLevelLinks = topLevelLinks; ResourceLinks = resourceLinks; RelationshipLinks = relationshipLinks; + Fields = Attributes.Cast().Concat(Relationships).ToArray(); foreach (ResourceFieldAttribute field in Fields) { @@ -126,7 +123,7 @@ 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}'."); + throw new InvalidOperationException($"Attribute for property '{propertyName}' does not exist on resource type '{ClrType.Name}'."); } public AttrAttribute TryGetAttributeByPropertyName(string propertyName) @@ -156,7 +153,7 @@ 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}'."); + throw new InvalidOperationException($"Relationship for property '{propertyName}' does not exist on resource type '{ClrType.Name}'."); } public RelationshipAttribute TryGetRelationshipByPropertyName(string propertyName) @@ -185,9 +182,9 @@ public override bool Equals(object obj) return false; } - var other = (ResourceContext)obj; + var other = (ResourceType)obj; - return PublicName == other.PublicName && ResourceType == other.ResourceType && IdentityType == other.IdentityType && + return PublicName == other.PublicName && ClrType == other.ClrType && IdentityClrType == other.IdentityClrType && Attributes.SequenceEqual(other.Attributes) && Relationships.SequenceEqual(other.Relationships) && EagerLoads.SequenceEqual(other.EagerLoads) && TopLevelLinks == other.TopLevelLinks && ResourceLinks == other.ResourceLinks && RelationshipLinks == other.RelationshipLinks; } @@ -197,8 +194,8 @@ public override int GetHashCode() var hashCode = new HashCode(); hashCode.Add(PublicName); - hashCode.Add(ResourceType); - hashCode.Add(IdentityType); + hashCode.Add(ClrType); + hashCode.Add(IdentityClrType); foreach (AttrAttribute attribute in Attributes) { diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index 2c8c1fc5a2..9accf54219 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -107,15 +107,15 @@ private static void RegisterForConstructedType(IServiceCollection services, Type // e.g. IResourceService is the shorthand for IResourceService bool isShorthandInterface = openGenericInterface.GetTypeInfo().GenericTypeParameters.Length == 1; - if (isShorthandInterface && resourceDescriptor.IdType != typeof(int)) + if (isShorthandInterface && resourceDescriptor.IdClrType != typeof(int)) { // We can't create a shorthand for ID types other than int. continue; } Type constructedType = isShorthandInterface - ? openGenericInterface.MakeGenericType(resourceDescriptor.ResourceType) - : openGenericInterface.MakeGenericType(resourceDescriptor.ResourceType, resourceDescriptor.IdType); + ? openGenericInterface.MakeGenericType(resourceDescriptor.ResourceClrType) + : openGenericInterface.MakeGenericType(resourceDescriptor.ResourceClrType, resourceDescriptor.IdClrType); if (constructedType.IsAssignableFrom(implementationType)) { diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index 7a179e5784..3a8e7bd8d5 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -137,14 +137,14 @@ private void AddDbContextResolvers(Assembly assembly) foreach (Type dbContextType in dbContextTypes) { - Type resolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); - _services.AddScoped(typeof(IDbContextResolver), resolverType); + Type dbContextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); + _services.AddScoped(typeof(IDbContextResolver), dbContextResolverType); } } private void AddResource(ResourceDescriptor resourceDescriptor) { - _resourceGraphBuilder.Add(resourceDescriptor.ResourceType, resourceDescriptor.IdType); + _resourceGraphBuilder.Add(resourceDescriptor.ResourceClrType, resourceDescriptor.IdClrType); } private void AddServices(Assembly assembly, ResourceDescriptor resourceDescriptor) @@ -174,8 +174,8 @@ private void AddResourceDefinitions(Assembly assembly, ResourceDescriptor resour private void RegisterImplementations(Assembly assembly, Type interfaceType, ResourceDescriptor resourceDescriptor) { Type[] genericArguments = interfaceType.GetTypeInfo().GenericTypeParameters.Length == 2 - ? ArrayFactory.Create(resourceDescriptor.ResourceType, resourceDescriptor.IdType) - : ArrayFactory.Create(resourceDescriptor.ResourceType); + ? ArrayFactory.Create(resourceDescriptor.ResourceClrType, resourceDescriptor.IdClrType) + : ArrayFactory.Create(resourceDescriptor.ResourceClrType); (Type implementation, Type registrationInterface)? result = _typeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, genericArguments); diff --git a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs index 7f6d266aa4..7f0e85371e 100644 --- a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs @@ -14,9 +14,9 @@ internal sealed class TypeLocator /// /// Attempts to lookup the ID type of the specified resource type. Returns null if it does not implement . /// - public Type TryGetIdType(Type resourceType) + public Type TryGetIdType(Type resourceClrType) { - Type identifiableInterface = resourceType.GetInterfaces().FirstOrDefault(@interface => + Type identifiableInterface = resourceClrType.GetInterfaces().FirstOrDefault(@interface => @interface.IsGenericType && @interface.GetGenericTypeDefinition() == typeof(IIdentifiable<>)); return identifiableInterface?.GetGenericArguments()[0]; diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs index e4bb444dc1..2960b23447 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs @@ -157,21 +157,21 @@ protected virtual void ValidateModelState(IEnumerable operat } } - private static void AddValidationErrors(ModelStateDictionary modelState, Type resourceType, int operationIndex, List violations) + private static void AddValidationErrors(ModelStateDictionary modelState, Type resourceClrType, int operationIndex, List violations) { foreach ((string propertyName, ModelStateEntry entry) in modelState) { - AddValidationErrors(entry, propertyName, resourceType, operationIndex, violations); + AddValidationErrors(entry, propertyName, resourceClrType, operationIndex, violations); } } - private static void AddValidationErrors(ModelStateEntry entry, string propertyName, Type resourceType, int operationIndex, + private static void AddValidationErrors(ModelStateEntry entry, string propertyName, Type resourceClrType, int operationIndex, List violations) { foreach (ModelError error in entry.Errors) { string prefix = $"/atomic:operations[{operationIndex}]/data/attributes/"; - var violation = new ModelStateViolation(prefix, propertyName, resourceType, error); + var violation = new ModelStateViolation(prefix, propertyName, resourceClrType, error); violations.Add(violation); } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs index 7ab85612c8..65d69c9d30 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs @@ -25,8 +25,8 @@ public abstract class JsonApiQueryController : BaseJsonApiContro /// /// Creates an instance from a read-only service. /// - protected JsonApiQueryController(IJsonApiOptions context, ILoggerFactory loggerFactory, IResourceQueryService queryService) - : base(context, loggerFactory, queryService) + protected JsonApiQueryController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceQueryService queryService) + : base(options, loggerFactory, queryService) { } diff --git a/src/JsonApiDotNetCore/Controllers/ModelStateViolation.cs b/src/JsonApiDotNetCore/Controllers/ModelStateViolation.cs index 2a4c8cfb84..49a935a7ef 100644 --- a/src/JsonApiDotNetCore/Controllers/ModelStateViolation.cs +++ b/src/JsonApiDotNetCore/Controllers/ModelStateViolation.cs @@ -12,19 +12,19 @@ public sealed class ModelStateViolation { public string Prefix { get; } public string PropertyName { get; } - public Type ResourceType { get; set; } + public Type ResourceClrType { get; set; } public ModelError Error { get; } - public ModelStateViolation(string prefix, string propertyName, Type resourceType, ModelError error) + public ModelStateViolation(string prefix, string propertyName, Type resourceClrType, ModelError error) { ArgumentGuard.NotNullNorEmpty(prefix, nameof(prefix)); ArgumentGuard.NotNullNorEmpty(propertyName, nameof(propertyName)); - ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(resourceClrType, nameof(resourceClrType)); ArgumentGuard.NotNull(error, nameof(error)); Prefix = prefix; PropertyName = propertyName; - ResourceType = resourceType; + ResourceClrType = resourceClrType; Error = error; } } diff --git a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs index 046f8de471..44066d430e 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs @@ -19,9 +19,9 @@ namespace JsonApiDotNetCore.Errors [PublicAPI] public sealed class InvalidModelStateException : JsonApiException { - public InvalidModelStateException(ModelStateDictionary modelState, Type resourceType, bool includeExceptionStackTraceInErrors, + public InvalidModelStateException(ModelStateDictionary modelState, Type resourceClrType, bool includeExceptionStackTraceInErrors, JsonNamingPolicy namingPolicy) - : this(FromModelStateDictionary(modelState, resourceType), includeExceptionStackTraceInErrors, namingPolicy) + : this(FromModelStateDictionary(modelState, resourceClrType), includeExceptionStackTraceInErrors, namingPolicy) { } @@ -30,26 +30,26 @@ public InvalidModelStateException(IEnumerable violations, b { } - private static IEnumerable FromModelStateDictionary(ModelStateDictionary modelState, Type resourceType) + private static IEnumerable FromModelStateDictionary(ModelStateDictionary modelState, Type resourceClrType) { ArgumentGuard.NotNull(modelState, nameof(modelState)); - ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(resourceClrType, nameof(resourceClrType)); var violations = new List(); foreach ((string propertyName, ModelStateEntry entry) in modelState) { - AddValidationErrors(entry, propertyName, resourceType, violations); + AddValidationErrors(entry, propertyName, resourceClrType, violations); } return violations; } - private static void AddValidationErrors(ModelStateEntry entry, string propertyName, Type resourceType, List violations) + private static void AddValidationErrors(ModelStateEntry entry, string propertyName, Type resourceClrType, List violations) { foreach (ModelError error in entry.Errors) { - var violation = new ModelStateViolation("/data/attributes/", propertyName, resourceType, error); + var violation = new ModelStateViolation("/data/attributes/", propertyName, resourceClrType, error); violations.Add(violation); } } @@ -74,16 +74,16 @@ private static IEnumerable FromModelStateViolation(ModelStateViolat } else { - string attributeName = GetDisplayNameForProperty(violation.PropertyName, violation.ResourceType, namingPolicy); + string attributeName = GetDisplayNameForProperty(violation.PropertyName, violation.ResourceClrType, namingPolicy); string attributePath = $"{violation.Prefix}{attributeName}"; yield return FromModelError(violation.Error, attributePath, includeExceptionStackTraceInErrors); } } - private static string GetDisplayNameForProperty(string propertyName, Type resourceType, JsonNamingPolicy namingPolicy) + private static string GetDisplayNameForProperty(string propertyName, Type resourceClrType, JsonNamingPolicy namingPolicy) { - PropertyInfo property = resourceType.GetProperty(propertyName); + PropertyInfo property = resourceClrType.GetProperty(propertyName); if (property != null) { diff --git a/src/JsonApiDotNetCore/Middleware/IControllerResourceMapping.cs b/src/JsonApiDotNetCore/Middleware/IControllerResourceMapping.cs index 4290b3b771..2ff155d324 100644 --- a/src/JsonApiDotNetCore/Middleware/IControllerResourceMapping.cs +++ b/src/JsonApiDotNetCore/Middleware/IControllerResourceMapping.cs @@ -1,4 +1,5 @@ using System; +using JsonApiDotNetCore.Configuration; namespace JsonApiDotNetCore.Middleware { @@ -10,11 +11,11 @@ public interface IControllerResourceMapping /// /// Gets the associated resource type for the provided controller type. /// - Type GetResourceTypeForController(Type controllerType); + ResourceType TryGetResourceTypeForController(Type controllerType); /// /// Gets the associated controller name for the provided resource type. /// - string GetControllerNameForResourceType(Type resourceType); + string TryGetControllerNameForResourceType(ResourceType resourceType); } } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs index 888c01544a..f3f7dfcf81 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs @@ -19,15 +19,15 @@ public interface IJsonApiRequest string PrimaryId { get; } /// - /// The primary (top-level) resource for this request. This would be "blogs" in "/blogs", "/blogs/123" or "/blogs/123/author". + /// The primary (top-level) resource type for this request. This would be "blogs" in "/blogs", "/blogs/123" or "/blogs/123/author". /// - ResourceContext PrimaryResource { get; } + ResourceType PrimaryResourceType { get; } /// - /// The secondary (nested) resource for this request. This would be null in "/blogs", "/blogs/123" and "/blogs/123/unknownResource" or "people" in + /// The secondary (nested) resource type for this request. This would be null in "/blogs", "/blogs/123" and "/blogs/123/unknownResource" or "people" in /// "/blogs/123/author" and "/blogs/123/relationships/author". /// - ResourceContext SecondaryResource { get; } + ResourceType SecondaryResourceType { get; } /// /// The relationship for this nested request. This would be null in "/blogs", "/blogs/123" and "/blogs/123/unknownResource" or "author" in diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 30ea5d9108..a41e172d18 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -39,13 +39,12 @@ public JsonApiMiddleware(RequestDelegate next, IHttpContextAccessor httpContextA } public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options, - IJsonApiRequest request, IResourceGraph resourceGraph, ILogger logger) + IJsonApiRequest request, ILogger logger) { ArgumentGuard.NotNull(httpContext, nameof(httpContext)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(logger, nameof(logger)); using (CodeTimingSessionManager.Current.Measure("JSON:API middleware")) @@ -56,9 +55,9 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin } RouteValueDictionary routeValues = httpContext.GetRouteData().Values; - ResourceContext primaryResourceContext = TryCreatePrimaryResourceContext(httpContext, controllerResourceMapping, resourceGraph); + ResourceType primaryResourceType = TryCreatePrimaryResourceType(httpContext, controllerResourceMapping); - if (primaryResourceContext != null) + if (primaryResourceType != null) { if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerWriteOptions) || !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerWriteOptions)) @@ -66,7 +65,7 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin return; } - SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, resourceGraph, httpContext.Request); + SetupResourceRequest((JsonApiRequest)request, primaryResourceType, routeValues, httpContext.Request); httpContext.RegisterJsonApiRequest(); } @@ -119,24 +118,14 @@ private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, Jso return true; } - private static ResourceContext TryCreatePrimaryResourceContext(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, - IResourceGraph resourceGraph) + private static ResourceType TryCreatePrimaryResourceType(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping) { Endpoint endpoint = httpContext.GetEndpoint(); var controllerActionDescriptor = endpoint?.Metadata.GetMetadata(); - if (controllerActionDescriptor != null) - { - Type controllerType = controllerActionDescriptor.ControllerTypeInfo; - Type resourceType = controllerResourceMapping.GetResourceTypeForController(controllerType); - - if (resourceType != null) - { - return resourceGraph.GetResourceContext(resourceType); - } - } - - return null; + return controllerActionDescriptor != null + ? controllerResourceMapping.TryGetResourceTypeForController(controllerActionDescriptor.ControllerTypeInfo) + : null; } private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, @@ -228,11 +217,11 @@ private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSeri await httpResponse.Body.FlushAsync(); } - private static void SetupResourceRequest(JsonApiRequest request, ResourceContext primaryResourceContext, RouteValueDictionary routeValues, - IResourceGraph resourceGraph, HttpRequest httpRequest) + private static void SetupResourceRequest(JsonApiRequest request, ResourceType primaryResourceType, RouteValueDictionary routeValues, + HttpRequest httpRequest) { request.IsReadOnly = httpRequest.Method == HttpMethod.Get.Method || httpRequest.Method == HttpMethod.Head.Method; - request.PrimaryResource = primaryResourceContext; + request.PrimaryResourceType = primaryResourceType; request.PrimaryId = GetPrimaryRequestId(routeValues); string relationshipName = GetRelationshipNameForSecondaryRequest(routeValues); @@ -252,12 +241,12 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceContext // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - RelationshipAttribute requestRelationship = primaryResourceContext.TryGetRelationshipByPublicName(relationshipName); + RelationshipAttribute requestRelationship = primaryResourceType.TryGetRelationshipByPublicName(relationshipName); if (requestRelationship != null) { request.Relationship = requestRelationship; - request.SecondaryResource = resourceGraph.GetResourceContext(requestRelationship.RightType); + request.SecondaryResourceType = requestRelationship.RightType; } } else diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs index 89bd6fa722..0c61b7ff38 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs @@ -15,10 +15,10 @@ public sealed class JsonApiRequest : IJsonApiRequest public string PrimaryId { get; set; } /// - public ResourceContext PrimaryResource { get; set; } + public ResourceType PrimaryResourceType { get; set; } /// - public ResourceContext SecondaryResource { get; set; } + public ResourceType SecondaryResourceType { get; set; } /// public RelationshipAttribute Relationship { get; set; } @@ -42,8 +42,8 @@ public void CopyFrom(IJsonApiRequest other) Kind = other.Kind; PrimaryId = other.PrimaryId; - PrimaryResource = other.PrimaryResource; - SecondaryResource = other.SecondaryResource; + PrimaryResourceType = other.PrimaryResourceType; + SecondaryResourceType = other.SecondaryResourceType; Relationship = other.Relationship; IsCollection = other.IsCollection; IsReadOnly = other.IsReadOnly; diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index af38f89dad..fee1edad0f 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -34,8 +34,8 @@ public sealed class JsonApiRoutingConvention : IJsonApiRoutingConvention private readonly IJsonApiOptions _options; private readonly IResourceGraph _resourceGraph; private readonly Dictionary _registeredControllerNameByTemplate = new(); - private readonly Dictionary _resourceContextPerControllerTypeMap = new(); - private readonly Dictionary _controllerPerResourceContextMap = new(); + private readonly Dictionary _resourceTypePerControllerTypeMap = new(); + private readonly Dictionary _controllerPerResourceTypeMap = new(); public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resourceGraph) { @@ -47,32 +47,19 @@ public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resource } /// - public Type GetResourceTypeForController(Type controllerType) + public ResourceType TryGetResourceTypeForController(Type controllerType) { ArgumentGuard.NotNull(controllerType, nameof(controllerType)); - if (_resourceContextPerControllerTypeMap.TryGetValue(controllerType, out ResourceContext resourceContext)) - { - return resourceContext.ResourceType; - } - - return null; + return _resourceTypePerControllerTypeMap.TryGetValue(controllerType, out ResourceType resourceType) ? resourceType : null; } /// - public string GetControllerNameForResourceType(Type resourceType) + public string TryGetControllerNameForResourceType(ResourceType resourceType) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); - - if (_controllerPerResourceContextMap.TryGetValue(resourceContext, out ControllerModel controllerModel)) - - { - return controllerModel.ControllerName; - } - - return null; + return _controllerPerResourceTypeMap.TryGetValue(resourceType, out ControllerModel controllerModel) ? controllerModel.ControllerName : null; } /// @@ -86,16 +73,16 @@ public void Apply(ApplicationModel application) if (!isOperationsController) { - Type resourceType = ExtractResourceTypeFromController(controller.ControllerType); + Type resourceClrType = ExtractResourceClrTypeFromController(controller.ControllerType); - if (resourceType != null) + if (resourceClrType != null) { - ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(resourceType); + ResourceType resourceType = _resourceGraph.TryGetResourceType(resourceClrType); - if (resourceContext != null) + if (resourceType != null) { - _resourceContextPerControllerTypeMap.Add(controller.ControllerType, resourceContext); - _controllerPerResourceContextMap.Add(resourceContext, controller); + _resourceTypePerControllerTypeMap.Add(controller.ControllerType, resourceType); + _controllerPerResourceTypeMap.Add(resourceType, controller); } } } @@ -133,9 +120,9 @@ private bool IsRoutingConventionEnabled(ControllerModel controller) /// private string TemplateFromResource(ControllerModel model) { - if (_resourceContextPerControllerTypeMap.TryGetValue(model.ControllerType, out ResourceContext resourceContext)) + if (_resourceTypePerControllerTypeMap.TryGetValue(model.ControllerType, out ResourceType resourceType)) { - return $"{_options.Namespace}/{resourceContext.PublicName}"; + return $"{_options.Namespace}/{resourceType.PublicName}"; } return null; @@ -156,7 +143,7 @@ private string TemplateFromController(ControllerModel model) /// /// Determines the resource associated to a controller by inspecting generic arguments in its inheritance tree. /// - private Type ExtractResourceTypeFromController(Type type) + private Type ExtractResourceClrTypeFromController(Type type) { Type aspNetControllerType = typeof(ControllerBase); Type coreControllerType = typeof(CoreJsonApiController); @@ -169,12 +156,12 @@ private Type ExtractResourceTypeFromController(Type type) if ((nextBaseType == aspNetControllerType || nextBaseType == coreControllerType) && currentType.IsGenericType) { - Type resourceType = currentType.GetGenericArguments() + Type resourceClrType = currentType.GetGenericArguments() .FirstOrDefault(typeArgument => typeArgument.IsOrImplementsInterface(typeof(IIdentifiable))); - if (resourceType != null) + if (resourceClrType != null) { - return resourceType; + return resourceClrType; } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs index 5efff034a6..e2049795f9 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs @@ -197,14 +197,14 @@ public override QueryExpression VisitSparseFieldTable(SparseFieldTableExpression { if (expression != null) { - ImmutableDictionary.Builder newTable = - ImmutableDictionary.CreateBuilder(); + ImmutableDictionary.Builder newTable = + ImmutableDictionary.CreateBuilder(); - foreach ((ResourceContext resourceContext, SparseFieldSetExpression sparseFieldSet) in expression.Table) + foreach ((ResourceType resourceType, SparseFieldSetExpression sparseFieldSet) in expression.Table) { if (Visit(sparseFieldSet, argument) is SparseFieldSetExpression newSparseFieldSet) { - newTable[resourceContext] = newSparseFieldSet; + newTable[resourceType] = newSparseFieldSet; } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs index 9cf7922349..4f1bca8127 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs @@ -12,9 +12,9 @@ namespace JsonApiDotNetCore.Queries.Expressions [PublicAPI] public class SparseFieldTableExpression : QueryExpression { - public IImmutableDictionary Table { get; } + public IImmutableDictionary Table { get; } - public SparseFieldTableExpression(IImmutableDictionary table) + public SparseFieldTableExpression(IImmutableDictionary table) { ArgumentGuard.NotNullNorEmpty(table, nameof(table), "entries"); @@ -30,7 +30,7 @@ public override string ToString() { var builder = new StringBuilder(); - foreach ((ResourceContext resource, SparseFieldSetExpression fields) in Table) + foreach ((ResourceType resource, SparseFieldSetExpression fields) in Table) { if (builder.Length > 0) { @@ -67,9 +67,9 @@ public override int GetHashCode() { var hashCode = new HashCode(); - foreach ((ResourceContext resourceContext, SparseFieldSetExpression sparseFieldSet) in Table) + foreach ((ResourceType resourceType, SparseFieldSetExpression sparseFieldSet) in Table) { - hashCode.Add(resourceContext); + hashCode.Add(resourceType); hashCode.Add(sparseFieldSet); } diff --git a/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs index c3fa8428e4..a6ef61605d 100644 --- a/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs @@ -14,34 +14,34 @@ public interface IQueryLayerComposer /// /// Builds a top-level filter from constraints, used to determine total resource count. /// - FilterExpression GetTopFilterFromConstraints(ResourceContext resourceContext); + FilterExpression GetTopFilterFromConstraints(ResourceType primaryResourceType); /// /// Collects constraints and builds a out of them, used to retrieve the actual resources. /// - QueryLayer ComposeFromConstraints(ResourceContext requestResource); + QueryLayer ComposeFromConstraints(ResourceType requestResourceType); /// /// Collects constraints and builds a out of them, used to retrieve one resource. /// - QueryLayer ComposeForGetById(TId id, ResourceContext resourceContext, TopFieldSelection fieldSelection); + QueryLayer ComposeForGetById(TId id, ResourceType primaryResourceType, TopFieldSelection fieldSelection); /// /// Collects constraints and builds the secondary layer for a relationship endpoint. /// - QueryLayer ComposeSecondaryLayerForRelationship(ResourceContext secondaryResourceContext); + QueryLayer ComposeSecondaryLayerForRelationship(ResourceType secondaryResourceType); /// /// Wraps a layer for a secondary endpoint into a primary layer, rewriting top-level includes. /// - QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, ResourceContext primaryResourceContext, TId primaryId, - RelationshipAttribute secondaryRelationship); + QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, ResourceType primaryResourceType, TId primaryId, + RelationshipAttribute relationship); /// /// Builds a query that retrieves the primary resource, including all of its attributes and all targeted relationships, during a create/update/delete /// request. /// - QueryLayer ComposeForUpdate(TId id, ResourceContext primaryResource); + QueryLayer ComposeForUpdate(TId id, ResourceType primaryResource); /// /// Builds a query for each targeted relationship with a filter to match on its right resource IDs. diff --git a/src/JsonApiDotNetCore/Queries/Internal/ISparseFieldSetCache.cs b/src/JsonApiDotNetCore/Queries/Internal/ISparseFieldSetCache.cs index 5571474e0f..cb3ab1f2d3 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/ISparseFieldSetCache.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/ISparseFieldSetCache.cs @@ -19,18 +19,18 @@ public interface ISparseFieldSetCache /// /// Gets the set of sparse fields to retrieve from the underlying data store. Returns an empty set to retrieve all fields. /// - IImmutableSet GetSparseFieldSetForQuery(ResourceContext resourceContext); + IImmutableSet GetSparseFieldSetForQuery(ResourceType resourceType); /// /// Gets the set of attributes to retrieve from the underlying data store for relationship endpoints. This always returns 'id', along with any additional /// attributes from resource definition callback. /// - IImmutableSet GetIdAttributeSetForRelationshipQuery(ResourceContext resourceContext); + IImmutableSet GetIdAttributeSetForRelationshipQuery(ResourceType resourceType); /// /// Gets the evaluated set of sparse fields to serialize into the response body. /// - IImmutableSet GetSparseFieldSetForSerializer(ResourceContext resourceContext); + IImmutableSet GetSparseFieldSetForSerializer(ResourceType resourceType); /// /// Resets the cached results from resource definition callbacks. diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs index d375f72a16..ef6ffd234e 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs @@ -13,28 +13,23 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing [PublicAPI] public class FilterParser : QueryExpressionParser { - private readonly IResourceGraph _resourceGraph; private readonly IResourceFactory _resourceFactory; - private readonly Action _validateSingleFieldCallback; - private ResourceContext _resourceContextInScope; + private readonly Action _validateSingleFieldCallback; + private ResourceType _resourceTypeInScope; - public FilterParser(IResourceGraph resourceGraph, IResourceFactory resourceFactory, - Action validateSingleFieldCallback = null) - : base(resourceGraph) + public FilterParser(IResourceFactory resourceFactory, Action validateSingleFieldCallback = null) { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); - _resourceGraph = resourceGraph; _resourceFactory = resourceFactory; _validateSingleFieldCallback = validateSingleFieldCallback; } - public FilterExpression Parse(string source, ResourceContext resourceContextInScope) + public FilterExpression Parse(string source, ResourceType resourceTypeInScope) { - ArgumentGuard.NotNull(resourceContextInScope, nameof(resourceContextInScope)); + ArgumentGuard.NotNull(resourceTypeInScope, nameof(resourceTypeInScope)); - _resourceContextInScope = resourceContextInScope; + _resourceTypeInScope = resourceTypeInScope; Tokenize(source); @@ -265,14 +260,13 @@ protected HasExpression ParseHas() private FilterExpression ParseFilterInHas(HasManyAttribute hasManyRelationship) { - ResourceContext outerScopeBackup = _resourceContextInScope; + ResourceType outerScopeBackup = _resourceTypeInScope; - Type innerResourceType = hasManyRelationship.RightType; - _resourceContextInScope = _resourceGraph.GetResourceContext(innerResourceType); + _resourceTypeInScope = hasManyRelationship.RightType; FilterExpression filter = ParseFilter(); - _resourceContextInScope = outerScopeBackup; + _resourceTypeInScope = outerScopeBackup; return filter; } @@ -337,9 +331,9 @@ protected LiteralConstantExpression ParseConstant() throw new QueryParseException("Value between quotes expected."); } - private string DeObfuscateStringId(Type resourceType, string stringId) + private string DeObfuscateStringId(Type resourceClrType, string stringId) { - IIdentifiable tempResource = _resourceFactory.CreateInstance(resourceType); + IIdentifiable tempResource = _resourceFactory.CreateInstance(resourceClrType); tempResource.StringId = stringId; return tempResource.GetTypedId().ToString(); } @@ -348,17 +342,17 @@ protected override IImmutableList OnResolveFieldChain(st { if (chainRequirements == FieldChainRequirements.EndsInToMany) { - return ChainResolver.ResolveToOneChainEndingInToMany(_resourceContextInScope, path, _validateSingleFieldCallback); + return ChainResolver.ResolveToOneChainEndingInToMany(_resourceTypeInScope, path, _validateSingleFieldCallback); } if (chainRequirements == FieldChainRequirements.EndsInAttribute) { - return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceContextInScope, path, _validateSingleFieldCallback); + return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceTypeInScope, path, _validateSingleFieldCallback); } if (chainRequirements.HasFlag(FieldChainRequirements.EndsInAttribute) && chainRequirements.HasFlag(FieldChainRequirements.EndsInToOne)) { - return ChainResolver.ResolveToOneChainEndingInAttributeOrToOne(_resourceContextInScope, path, _validateSingleFieldCallback); + return ChainResolver.ResolveToOneChainEndingInAttributeOrToOne(_resourceTypeInScope, path, _validateSingleFieldCallback); } throw new InvalidOperationException($"Unexpected combination of chain requirement flags '{chainRequirements}'."); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs index 9d6f394d75..1646c3bcb0 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs @@ -14,20 +14,19 @@ public class IncludeParser : QueryExpressionParser { private static readonly IncludeChainConverter IncludeChainConverter = new(); - private readonly Action _validateSingleRelationshipCallback; - private ResourceContext _resourceContextInScope; + private readonly Action _validateSingleRelationshipCallback; + private ResourceType _resourceTypeInScope; - public IncludeParser(IResourceGraph resourceGraph, Action validateSingleRelationshipCallback = null) - : base(resourceGraph) + public IncludeParser(Action validateSingleRelationshipCallback = null) { _validateSingleRelationshipCallback = validateSingleRelationshipCallback; } - public IncludeExpression Parse(string source, ResourceContext resourceContextInScope, int? maximumDepth) + public IncludeExpression Parse(string source, ResourceType resourceTypeInScope, int? maximumDepth) { - ArgumentGuard.NotNull(resourceContextInScope, nameof(resourceContextInScope)); + ArgumentGuard.NotNull(resourceTypeInScope, nameof(resourceTypeInScope)); - _resourceContextInScope = resourceContextInScope; + _resourceTypeInScope = resourceTypeInScope; Tokenize(source); @@ -74,7 +73,7 @@ private static void ValidateMaximumIncludeDepth(int? maximumDepth, IEnumerable OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { - return ChainResolver.ResolveRelationshipChain(_resourceContextInScope, path, _validateSingleRelationshipCallback); + return ChainResolver.ResolveRelationshipChain(_resourceTypeInScope, path, _validateSingleRelationshipCallback); } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs index 62f8dd6a91..980ec8450a 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs @@ -11,20 +11,19 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing [PublicAPI] public class PaginationParser : QueryExpressionParser { - private readonly Action _validateSingleFieldCallback; - private ResourceContext _resourceContextInScope; + private readonly Action _validateSingleFieldCallback; + private ResourceType _resourceTypeInScope; - public PaginationParser(IResourceGraph resourceGraph, Action validateSingleFieldCallback = null) - : base(resourceGraph) + public PaginationParser(Action validateSingleFieldCallback = null) { _validateSingleFieldCallback = validateSingleFieldCallback; } - public PaginationQueryStringValueExpression Parse(string source, ResourceContext resourceContextInScope) + public PaginationQueryStringValueExpression Parse(string source, ResourceType resourceTypeInScope) { - ArgumentGuard.NotNull(resourceContextInScope, nameof(resourceContextInScope)); + ArgumentGuard.NotNull(resourceTypeInScope, nameof(resourceTypeInScope)); - _resourceContextInScope = resourceContextInScope; + _resourceTypeInScope = resourceTypeInScope; Tokenize(source); @@ -107,7 +106,7 @@ protected PaginationElementQueryStringValueExpression ParsePaginationElement() protected override IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { - return ChainResolver.ResolveToManyChain(_resourceContextInScope, path, _validateSingleFieldCallback); + return ChainResolver.ResolveToManyChain(_resourceTypeInScope, path, _validateSingleFieldCallback); } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs index 65cf321347..b33e01aaf3 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs @@ -2,7 +2,6 @@ using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; @@ -21,9 +20,9 @@ public abstract class QueryExpressionParser protected Stack TokenStack { get; private set; } private protected ResourceFieldChainResolver ChainResolver { get; } - protected QueryExpressionParser(IResourceGraph resourceGraph) + protected QueryExpressionParser() { - ChainResolver = new ResourceFieldChainResolver(resourceGraph); + ChainResolver = new ResourceFieldChainResolver(); } /// diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs index 7d98110362..7d9f848862 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs @@ -11,22 +11,21 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing public class QueryStringParameterScopeParser : QueryExpressionParser { private readonly FieldChainRequirements _chainRequirements; - private readonly Action _validateSingleFieldCallback; - private ResourceContext _resourceContextInScope; + private readonly Action _validateSingleFieldCallback; + private ResourceType _resourceTypeInScope; - public QueryStringParameterScopeParser(IResourceGraph resourceGraph, FieldChainRequirements chainRequirements, - Action validateSingleFieldCallback = null) - : base(resourceGraph) + public QueryStringParameterScopeParser(FieldChainRequirements chainRequirements, + Action validateSingleFieldCallback = null) { _chainRequirements = chainRequirements; _validateSingleFieldCallback = validateSingleFieldCallback; } - public QueryStringParameterScopeExpression Parse(string source, ResourceContext resourceContextInScope) + public QueryStringParameterScopeExpression Parse(string source, ResourceType resourceTypeInScope) { - ArgumentGuard.NotNull(resourceContextInScope, nameof(resourceContextInScope)); + ArgumentGuard.NotNull(resourceTypeInScope, nameof(resourceTypeInScope)); - _resourceContextInScope = resourceContextInScope; + _resourceTypeInScope = resourceTypeInScope; Tokenize(source); @@ -65,12 +64,12 @@ protected override IImmutableList OnResolveFieldChain(st if (chainRequirements == FieldChainRequirements.EndsInToMany) { // The mismatch here (ends-in-to-many being interpreted as entire-chain-must-be-to-many) is intentional. - return ChainResolver.ResolveToManyChain(_resourceContextInScope, path, _validateSingleFieldCallback); + return ChainResolver.ResolveToManyChain(_resourceTypeInScope, path, _validateSingleFieldCallback); } if (chainRequirements == FieldChainRequirements.IsRelationship) { - return ChainResolver.ResolveRelationshipChain(_resourceContextInScope, path, _validateSingleFieldCallback); + return ChainResolver.ResolveRelationshipChain(_resourceTypeInScope, path, _validateSingleFieldCallback); } throw new InvalidOperationException($"Unexpected combination of chain requirement flags '{chainRequirements}'."); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs index edb7a774f2..6b0900ef96 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs @@ -11,40 +11,31 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing /// internal sealed class ResourceFieldChainResolver { - private readonly IResourceGraph _resourceGraph; - - public ResourceFieldChainResolver(IResourceGraph resourceGraph) - { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - - _resourceGraph = resourceGraph; - } - /// /// Resolves a chain of relationships that ends in a to-many relationship, for example: blogs.owner.articles.comments /// - public IImmutableList ResolveToManyChain(ResourceContext resourceContext, string path, - Action validateCallback = null) + public IImmutableList ResolveToManyChain(ResourceType resourceType, string path, + Action validateCallback = null) { ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); string[] publicNameParts = path.Split("."); - ResourceContext nextResourceContext = resourceContext; + ResourceType nextResourceType = resourceType; foreach (string publicName in publicNameParts[..^1]) { - RelationshipAttribute relationship = GetRelationship(publicName, nextResourceContext, path); + RelationshipAttribute relationship = GetRelationship(publicName, nextResourceType, path); - validateCallback?.Invoke(relationship, nextResourceContext, path); + validateCallback?.Invoke(relationship, nextResourceType, path); chainBuilder.Add(relationship); - nextResourceContext = _resourceGraph.GetResourceContext(relationship.RightType); + nextResourceType = relationship.RightType; } string lastName = publicNameParts[^1]; - RelationshipAttribute lastToManyRelationship = GetToManyRelationship(lastName, nextResourceContext, path); + RelationshipAttribute lastToManyRelationship = GetToManyRelationship(lastName, nextResourceType, path); - validateCallback?.Invoke(lastToManyRelationship, nextResourceContext, path); + validateCallback?.Invoke(lastToManyRelationship, nextResourceType, path); chainBuilder.Add(lastToManyRelationship); return chainBuilder.ToImmutable(); @@ -62,20 +53,20 @@ public IImmutableList ResolveToManyChain(ResourceContext /// articles.revisions.author /// /// - public IImmutableList ResolveRelationshipChain(ResourceContext resourceContext, string path, - Action validateCallback = null) + public IImmutableList ResolveRelationshipChain(ResourceType resourceType, string path, + Action validateCallback = null) { ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); - ResourceContext nextResourceContext = resourceContext; + ResourceType nextResourceType = resourceType; foreach (string publicName in path.Split(".")) { - RelationshipAttribute relationship = GetRelationship(publicName, nextResourceContext, path); + RelationshipAttribute relationship = GetRelationship(publicName, nextResourceType, path); - validateCallback?.Invoke(relationship, nextResourceContext, path); + validateCallback?.Invoke(relationship, nextResourceType, path); chainBuilder.Add(relationship); - nextResourceContext = _resourceGraph.GetResourceContext(relationship.RightType); + nextResourceType = relationship.RightType; } return chainBuilder.ToImmutable(); @@ -88,28 +79,28 @@ public IImmutableList ResolveRelationshipChain(ResourceC /// /// name /// - public IImmutableList ResolveToOneChainEndingInAttribute(ResourceContext resourceContext, string path, - Action validateCallback = null) + public IImmutableList ResolveToOneChainEndingInAttribute(ResourceType resourceType, string path, + Action validateCallback = null) { ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); string[] publicNameParts = path.Split("."); - ResourceContext nextResourceContext = resourceContext; + ResourceType nextResourceType = resourceType; foreach (string publicName in publicNameParts[..^1]) { - RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceContext, path); + RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceType, path); - validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); + validateCallback?.Invoke(toOneRelationship, nextResourceType, path); chainBuilder.Add(toOneRelationship); - nextResourceContext = _resourceGraph.GetResourceContext(toOneRelationship.RightType); + nextResourceType = toOneRelationship.RightType; } string lastName = publicNameParts[^1]; - AttrAttribute lastAttribute = GetAttribute(lastName, nextResourceContext, path); + AttrAttribute lastAttribute = GetAttribute(lastName, nextResourceType, path); - validateCallback?.Invoke(lastAttribute, nextResourceContext, path); + validateCallback?.Invoke(lastAttribute, nextResourceType, path); chainBuilder.Add(lastAttribute); return chainBuilder.ToImmutable(); @@ -124,29 +115,29 @@ public IImmutableList ResolveToOneChainEndingInAttribute /// comments /// /// - public IImmutableList ResolveToOneChainEndingInToMany(ResourceContext resourceContext, string path, - Action validateCallback = null) + public IImmutableList ResolveToOneChainEndingInToMany(ResourceType resourceType, string path, + Action validateCallback = null) { ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); string[] publicNameParts = path.Split("."); - ResourceContext nextResourceContext = resourceContext; + ResourceType nextResourceType = resourceType; foreach (string publicName in publicNameParts[..^1]) { - RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceContext, path); + RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceType, path); - validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); + validateCallback?.Invoke(toOneRelationship, nextResourceType, path); chainBuilder.Add(toOneRelationship); - nextResourceContext = _resourceGraph.GetResourceContext(toOneRelationship.RightType); + nextResourceType = toOneRelationship.RightType; } string lastName = publicNameParts[^1]; - RelationshipAttribute toManyRelationship = GetToManyRelationship(lastName, nextResourceContext, path); + RelationshipAttribute toManyRelationship = GetToManyRelationship(lastName, nextResourceType, path); - validateCallback?.Invoke(toManyRelationship, nextResourceContext, path); + validateCallback?.Invoke(toManyRelationship, nextResourceType, path); chainBuilder.Add(toManyRelationship); return chainBuilder.ToImmutable(); @@ -161,105 +152,105 @@ public IImmutableList ResolveToOneChainEndingInToMany(Re /// author.address /// ///
- public IImmutableList ResolveToOneChainEndingInAttributeOrToOne(ResourceContext resourceContext, string path, - Action validateCallback = null) + public IImmutableList ResolveToOneChainEndingInAttributeOrToOne(ResourceType resourceType, string path, + Action validateCallback = null) { ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); string[] publicNameParts = path.Split("."); - ResourceContext nextResourceContext = resourceContext; + ResourceType nextResourceType = resourceType; foreach (string publicName in publicNameParts[..^1]) { - RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceContext, path); + RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceType, path); - validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); + validateCallback?.Invoke(toOneRelationship, nextResourceType, path); chainBuilder.Add(toOneRelationship); - nextResourceContext = _resourceGraph.GetResourceContext(toOneRelationship.RightType); + nextResourceType = toOneRelationship.RightType; } string lastName = publicNameParts[^1]; - ResourceFieldAttribute lastField = GetField(lastName, nextResourceContext, path); + ResourceFieldAttribute lastField = GetField(lastName, nextResourceType, path); if (lastField is HasManyAttribute) { throw new QueryParseException(path == lastName - ? $"Field '{lastName}' must be an attribute or a to-one relationship on resource '{nextResourceContext.PublicName}'." - : $"Field '{lastName}' in '{path}' must be an attribute or a to-one relationship on resource '{nextResourceContext.PublicName}'."); + ? $"Field '{lastName}' must be an attribute or a to-one relationship on resource type '{nextResourceType.PublicName}'." + : $"Field '{lastName}' in '{path}' must be an attribute or a to-one relationship on resource type '{nextResourceType.PublicName}'."); } - validateCallback?.Invoke(lastField, nextResourceContext, path); + validateCallback?.Invoke(lastField, nextResourceType, path); chainBuilder.Add(lastField); return chainBuilder.ToImmutable(); } - private RelationshipAttribute GetRelationship(string publicName, ResourceContext resourceContext, string path) + private RelationshipAttribute GetRelationship(string publicName, ResourceType resourceType, string path) { - RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(publicName); + RelationshipAttribute relationship = resourceType.TryGetRelationshipByPublicName(publicName); if (relationship == null) { throw new QueryParseException(path == publicName - ? $"Relationship '{publicName}' does not exist on resource '{resourceContext.PublicName}'." - : $"Relationship '{publicName}' in '{path}' does not exist on resource '{resourceContext.PublicName}'."); + ? $"Relationship '{publicName}' does not exist on resource type '{resourceType.PublicName}'." + : $"Relationship '{publicName}' in '{path}' does not exist on resource type '{resourceType.PublicName}'."); } return relationship; } - private RelationshipAttribute GetToManyRelationship(string publicName, ResourceContext resourceContext, string path) + private RelationshipAttribute GetToManyRelationship(string publicName, ResourceType resourceType, string path) { - RelationshipAttribute relationship = GetRelationship(publicName, resourceContext, path); + RelationshipAttribute relationship = GetRelationship(publicName, resourceType, path); if (relationship is not HasManyAttribute) { throw new QueryParseException(path == publicName - ? $"Relationship '{publicName}' must be a to-many relationship on resource '{resourceContext.PublicName}'." - : $"Relationship '{publicName}' in '{path}' must be a to-many relationship on resource '{resourceContext.PublicName}'."); + ? $"Relationship '{publicName}' must be a to-many relationship on resource type '{resourceType.PublicName}'." + : $"Relationship '{publicName}' in '{path}' must be a to-many relationship on resource type '{resourceType.PublicName}'."); } return relationship; } - private RelationshipAttribute GetToOneRelationship(string publicName, ResourceContext resourceContext, string path) + private RelationshipAttribute GetToOneRelationship(string publicName, ResourceType resourceType, string path) { - RelationshipAttribute relationship = GetRelationship(publicName, resourceContext, path); + RelationshipAttribute relationship = GetRelationship(publicName, resourceType, path); if (relationship is not HasOneAttribute) { throw new QueryParseException(path == publicName - ? $"Relationship '{publicName}' must be a to-one relationship on resource '{resourceContext.PublicName}'." - : $"Relationship '{publicName}' in '{path}' must be a to-one relationship on resource '{resourceContext.PublicName}'."); + ? $"Relationship '{publicName}' must be a to-one relationship on resource type '{resourceType.PublicName}'." + : $"Relationship '{publicName}' in '{path}' must be a to-one relationship on resource type '{resourceType.PublicName}'."); } return relationship; } - private AttrAttribute GetAttribute(string publicName, ResourceContext resourceContext, string path) + private AttrAttribute GetAttribute(string publicName, ResourceType resourceType, string path) { - AttrAttribute attribute = resourceContext.TryGetAttributeByPublicName(publicName); + AttrAttribute attribute = resourceType.TryGetAttributeByPublicName(publicName); if (attribute == null) { throw new QueryParseException(path == publicName - ? $"Attribute '{publicName}' does not exist on resource '{resourceContext.PublicName}'." - : $"Attribute '{publicName}' in '{path}' does not exist on resource '{resourceContext.PublicName}'."); + ? $"Attribute '{publicName}' does not exist on resource type '{resourceType.PublicName}'." + : $"Attribute '{publicName}' in '{path}' does not exist on resource type '{resourceType.PublicName}'."); } return attribute; } - public ResourceFieldAttribute GetField(string publicName, ResourceContext resourceContext, string path) + public ResourceFieldAttribute GetField(string publicName, ResourceType resourceType, string path) { - ResourceFieldAttribute field = resourceContext.Fields.FirstOrDefault(nextField => nextField.PublicName == publicName); + ResourceFieldAttribute field = resourceType.Fields.FirstOrDefault(nextField => nextField.PublicName == publicName); if (field == null) { throw new QueryParseException(path == publicName - ? $"Field '{publicName}' does not exist on resource '{resourceContext.PublicName}'." - : $"Field '{publicName}' in '{path}' does not exist on resource '{resourceContext.PublicName}'."); + ? $"Field '{publicName}' does not exist on resource type '{resourceType.PublicName}'." + : $"Field '{publicName}' in '{path}' does not exist on resource type '{resourceType.PublicName}'."); } return field; diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs index 4d588bacbb..4b14f2d996 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs @@ -11,20 +11,19 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing [PublicAPI] public class SortParser : QueryExpressionParser { - private readonly Action _validateSingleFieldCallback; - private ResourceContext _resourceContextInScope; + private readonly Action _validateSingleFieldCallback; + private ResourceType _resourceTypeInScope; - public SortParser(IResourceGraph resourceGraph, Action validateSingleFieldCallback = null) - : base(resourceGraph) + public SortParser(Action validateSingleFieldCallback = null) { _validateSingleFieldCallback = validateSingleFieldCallback; } - public SortExpression Parse(string source, ResourceContext resourceContextInScope) + public SortExpression Parse(string source, ResourceType resourceTypeInScope) { - ArgumentGuard.NotNull(resourceContextInScope, nameof(resourceContextInScope)); + ArgumentGuard.NotNull(resourceTypeInScope, nameof(resourceTypeInScope)); - _resourceContextInScope = resourceContextInScope; + _resourceTypeInScope = resourceTypeInScope; Tokenize(source); @@ -79,12 +78,12 @@ protected override IImmutableList OnResolveFieldChain(st { if (chainRequirements == FieldChainRequirements.EndsInToMany) { - return ChainResolver.ResolveToOneChainEndingInToMany(_resourceContextInScope, path); + return ChainResolver.ResolveToOneChainEndingInToMany(_resourceTypeInScope, path); } if (chainRequirements == FieldChainRequirements.EndsInAttribute) { - return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceContextInScope, path, _validateSingleFieldCallback); + return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceTypeInScope, path, _validateSingleFieldCallback); } throw new InvalidOperationException($"Unexpected combination of chain requirement flags '{chainRequirements}'."); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs index ea44dca7e5..be8daf350c 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs @@ -11,20 +11,19 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing [PublicAPI] public class SparseFieldSetParser : QueryExpressionParser { - private readonly Action _validateSingleFieldCallback; - private ResourceContext _resourceContext; + private readonly Action _validateSingleFieldCallback; + private ResourceType _resourceType; - public SparseFieldSetParser(IResourceGraph resourceGraph, Action validateSingleFieldCallback = null) - : base(resourceGraph) + public SparseFieldSetParser(Action validateSingleFieldCallback = null) { _validateSingleFieldCallback = validateSingleFieldCallback; } - public SparseFieldSetExpression Parse(string source, ResourceContext resourceContext) + public SparseFieldSetExpression Parse(string source, ResourceType resourceType) { - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - _resourceContext = resourceContext; + _resourceType = resourceType; Tokenize(source); @@ -56,9 +55,9 @@ protected SparseFieldSetExpression ParseSparseFieldSet() protected override IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { - ResourceFieldAttribute field = ChainResolver.GetField(path, _resourceContext, path); + ResourceFieldAttribute field = ChainResolver.GetField(path, _resourceType, path); - _validateSingleFieldCallback?.Invoke(field, _resourceContext, path); + _validateSingleFieldCallback?.Invoke(field, _resourceType, path); return ImmutableArray.Create(field); } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs index fec5356282..d071514a1a 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs @@ -12,23 +12,24 @@ public class SparseFieldTypeParser : QueryExpressionParser private readonly IResourceGraph _resourceGraph; public SparseFieldTypeParser(IResourceGraph resourceGraph) - : base(resourceGraph) { + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + _resourceGraph = resourceGraph; } - public ResourceContext Parse(string source) + public ResourceType Parse(string source) { Tokenize(source); - ResourceContext resourceContext = ParseSparseFieldTarget(); + ResourceType resourceType = ParseSparseFieldTarget(); AssertTokenStackIsEmpty(); - return resourceContext; + return resourceType; } - private ResourceContext ParseSparseFieldTarget() + private ResourceType ParseSparseFieldTarget() { if (!TokenStack.TryPop(out Token token) || token.Kind != TokenKind.Text) { @@ -37,33 +38,33 @@ private ResourceContext ParseSparseFieldTarget() EatSingleCharacterToken(TokenKind.OpenBracket); - ResourceContext resourceContext = ParseResourceName(); + ResourceType resourceType = ParseResourceName(); EatSingleCharacterToken(TokenKind.CloseBracket); - return resourceContext; + return resourceType; } - private ResourceContext ParseResourceName() + private ResourceType ParseResourceName() { if (TokenStack.TryPop(out Token token) && token.Kind == TokenKind.Text) { - return GetResourceContext(token.Value); + return GetResourceType(token.Value); } throw new QueryParseException("Resource type expected."); } - private ResourceContext GetResourceContext(string publicName) + private ResourceType GetResourceType(string publicName) { - ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(publicName); + ResourceType resourceType = _resourceGraph.TryGetResourceType(publicName); - if (resourceContext == null) + if (resourceType == null) { throw new QueryParseException($"Resource type '{publicName}' does not exist."); } - return resourceContext; + return resourceType; } protected override IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index c49e47d0b0..165ce5b032 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -17,7 +17,6 @@ public class QueryLayerComposer : IQueryLayerComposer { private readonly CollectionConverter _collectionConverter = new(); private readonly IEnumerable _constraintProviders; - private readonly IResourceGraph _resourceGraph; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly IJsonApiOptions _options; private readonly IPaginationContext _paginationContext; @@ -25,12 +24,11 @@ public class QueryLayerComposer : IQueryLayerComposer private readonly IEvaluatedIncludeCache _evaluatedIncludeCache; private readonly ISparseFieldSetCache _sparseFieldSetCache; - public QueryLayerComposer(IEnumerable constraintProviders, IResourceGraph resourceGraph, - IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiOptions options, IPaginationContext paginationContext, - ITargetedFields targetedFields, IEvaluatedIncludeCache evaluatedIncludeCache, ISparseFieldSetCache sparseFieldSetCache) + public QueryLayerComposer(IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, + IJsonApiOptions options, IPaginationContext paginationContext, ITargetedFields targetedFields, IEvaluatedIncludeCache evaluatedIncludeCache, + ISparseFieldSetCache sparseFieldSetCache) { ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(paginationContext, nameof(paginationContext)); @@ -39,7 +37,6 @@ public QueryLayerComposer(IEnumerable constraintProvid ArgumentGuard.NotNull(sparseFieldSetCache, nameof(sparseFieldSetCache)); _constraintProviders = constraintProviders; - _resourceGraph = resourceGraph; _resourceDefinitionAccessor = resourceDefinitionAccessor; _options = options; _paginationContext = paginationContext; @@ -49,7 +46,7 @@ public QueryLayerComposer(IEnumerable constraintProvid } /// - public FilterExpression GetTopFilterFromConstraints(ResourceContext resourceContext) + public FilterExpression GetTopFilterFromConstraints(ResourceType primaryResourceType) { ExpressionInScope[] constraints = _constraintProviders.SelectMany(provider => provider.GetConstraints()).ToArray(); @@ -65,17 +62,17 @@ public FilterExpression GetTopFilterFromConstraints(ResourceContext resourceCont // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - return GetFilter(filtersInTopScope, resourceContext); + return GetFilter(filtersInTopScope, primaryResourceType); } /// - public QueryLayer ComposeFromConstraints(ResourceContext requestResource) + public QueryLayer ComposeFromConstraints(ResourceType requestResourceType) { - ArgumentGuard.NotNull(requestResource, nameof(requestResource)); + ArgumentGuard.NotNull(requestResourceType, nameof(requestResourceType)); ExpressionInScope[] constraints = _constraintProviders.SelectMany(provider => provider.GetConstraints()).ToArray(); - QueryLayer topLayer = ComposeTopLayer(constraints, requestResource); + QueryLayer topLayer = ComposeTopLayer(constraints, requestResourceType); topLayer.Include = ComposeChildren(topLayer, constraints); _evaluatedIncludeCache.Set(topLayer.Include); @@ -83,7 +80,7 @@ public QueryLayer ComposeFromConstraints(ResourceContext requestResource) return topLayer; } - private QueryLayer ComposeTopLayer(IEnumerable constraints, ResourceContext resourceContext) + private QueryLayer ComposeTopLayer(IEnumerable constraints, ResourceType resourceType) { using IDisposable _ = CodeTimingSessionManager.Current.Measure("Top-level query composition"); @@ -98,7 +95,7 @@ private QueryLayer ComposeTopLayer(IEnumerable constraints, R // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - PaginationExpression topPagination = GetPagination(expressionsInTopScope, resourceContext); + PaginationExpression topPagination = GetPagination(expressionsInTopScope, resourceType); if (topPagination != null) { @@ -106,12 +103,12 @@ private QueryLayer ComposeTopLayer(IEnumerable constraints, R _paginationContext.PageNumber = topPagination.PageNumber; } - return new QueryLayer(resourceContext) + return new QueryLayer(resourceType) { - Filter = GetFilter(expressionsInTopScope, resourceContext), - Sort = GetSort(expressionsInTopScope, resourceContext), + Filter = GetFilter(expressionsInTopScope, resourceType), + Sort = GetSort(expressionsInTopScope, resourceType), Pagination = ((JsonApiOptions)_options).DisableTopPagination ? null : topPagination, - Projection = GetProjectionForSparseAttributeSet(resourceContext) + Projection = GetProjectionForSparseAttributeSet(resourceType) }; } @@ -143,7 +140,7 @@ private IImmutableSet ProcessIncludeSet(IImmutableSet< ICollection parentRelationshipChain, ICollection constraints) { IImmutableSet includeElementsEvaluated = - GetIncludeElements(includeElements, parentLayer.ResourceContext) ?? ImmutableHashSet.Empty; + GetIncludeElements(includeElements, parentLayer.ResourceType) ?? ImmutableHashSet.Empty; var updatesInChildren = new Dictionary>(); @@ -170,23 +167,22 @@ private IImmutableSet ProcessIncludeSet(IImmutableSet< // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - ResourceContext resourceContext = _resourceGraph.GetResourceContext(includeElement.Relationship.RightType); + ResourceType resourceType = includeElement.Relationship.RightType; bool isToManyRelationship = includeElement.Relationship is HasManyAttribute; - var child = new QueryLayer(resourceContext) + var child = new QueryLayer(resourceType) { - Filter = isToManyRelationship ? GetFilter(expressionsInCurrentScope, resourceContext) : null, - Sort = isToManyRelationship ? GetSort(expressionsInCurrentScope, resourceContext) : null, + Filter = isToManyRelationship ? GetFilter(expressionsInCurrentScope, resourceType) : null, + Sort = isToManyRelationship ? GetSort(expressionsInCurrentScope, resourceType) : null, Pagination = isToManyRelationship - ? ((JsonApiOptions)_options).DisableChildrenPagination ? null : GetPagination(expressionsInCurrentScope, resourceContext) + ? ((JsonApiOptions)_options).DisableChildrenPagination ? null : GetPagination(expressionsInCurrentScope, resourceType) : null, - Projection = GetProjectionForSparseAttributeSet(resourceContext) + Projection = GetProjectionForSparseAttributeSet(resourceType) }; parentLayer.Projection.Add(includeElement.Relationship, child); - IImmutableSet updatedChildren = - ProcessIncludeSet(includeElement.Children, child, relationshipChain, constraints); + IImmutableSet updatedChildren = ProcessIncludeSet(includeElement.Children, child, relationshipChain, constraints); if (!ReferenceEquals(includeElement.Children, updatedChildren)) { @@ -214,13 +210,13 @@ private static IImmutableSet ApplyIncludeElementUpdate } /// - public QueryLayer ComposeForGetById(TId id, ResourceContext resourceContext, TopFieldSelection fieldSelection) + public QueryLayer ComposeForGetById(TId id, ResourceType primaryResourceType, TopFieldSelection fieldSelection) { - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(primaryResourceType, nameof(primaryResourceType)); - AttrAttribute idAttribute = GetIdAttribute(resourceContext); + AttrAttribute idAttribute = GetIdAttribute(primaryResourceType); - QueryLayer queryLayer = ComposeFromConstraints(resourceContext); + QueryLayer queryLayer = ComposeFromConstraints(primaryResourceType); queryLayer.Sort = null; queryLayer.Pagination = null; queryLayer.Filter = CreateFilterByIds(id.AsArray(), idAttribute, queryLayer.Filter); @@ -245,48 +241,48 @@ public QueryLayer ComposeForGetById(TId id, ResourceContext resourceContext } /// - public QueryLayer ComposeSecondaryLayerForRelationship(ResourceContext secondaryResourceContext) + public QueryLayer ComposeSecondaryLayerForRelationship(ResourceType secondaryResourceType) { - ArgumentGuard.NotNull(secondaryResourceContext, nameof(secondaryResourceContext)); + ArgumentGuard.NotNull(secondaryResourceType, nameof(secondaryResourceType)); - QueryLayer secondaryLayer = ComposeFromConstraints(secondaryResourceContext); - secondaryLayer.Projection = GetProjectionForRelationship(secondaryResourceContext); + QueryLayer secondaryLayer = ComposeFromConstraints(secondaryResourceType); + secondaryLayer.Projection = GetProjectionForRelationship(secondaryResourceType); secondaryLayer.Include = null; return secondaryLayer; } - private IDictionary GetProjectionForRelationship(ResourceContext secondaryResourceContext) + private IDictionary GetProjectionForRelationship(ResourceType secondaryResourceType) { - IImmutableSet secondaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(secondaryResourceContext); + IImmutableSet secondaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(secondaryResourceType); return secondaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer)null); } /// - public QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, ResourceContext primaryResourceContext, TId primaryId, - RelationshipAttribute secondaryRelationship) + public QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, ResourceType primaryResourceType, TId primaryId, + RelationshipAttribute relationship) { ArgumentGuard.NotNull(secondaryLayer, nameof(secondaryLayer)); - ArgumentGuard.NotNull(primaryResourceContext, nameof(primaryResourceContext)); - ArgumentGuard.NotNull(secondaryRelationship, nameof(secondaryRelationship)); + ArgumentGuard.NotNull(primaryResourceType, nameof(primaryResourceType)); + ArgumentGuard.NotNull(relationship, nameof(relationship)); IncludeExpression innerInclude = secondaryLayer.Include; secondaryLayer.Include = null; - IImmutableSet primaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(primaryResourceContext); + IImmutableSet primaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(primaryResourceType); Dictionary primaryProjection = primaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer)null); - primaryProjection[secondaryRelationship] = secondaryLayer; + primaryProjection[relationship] = secondaryLayer; - FilterExpression primaryFilter = GetFilter(Array.Empty(), primaryResourceContext); - AttrAttribute primaryIdAttribute = GetIdAttribute(primaryResourceContext); + FilterExpression primaryFilter = GetFilter(Array.Empty(), primaryResourceType); + AttrAttribute primaryIdAttribute = GetIdAttribute(primaryResourceType); - return new QueryLayer(primaryResourceContext) + return new QueryLayer(primaryResourceType) { - Include = RewriteIncludeForSecondaryEndpoint(innerInclude, secondaryRelationship), + Include = RewriteIncludeForSecondaryEndpoint(innerInclude, relationship), Filter = CreateFilterByIds(primaryId.AsArray(), primaryIdAttribute, primaryFilter), Projection = primaryProjection }; @@ -322,7 +318,7 @@ private FilterExpression CreateFilterByIds(IReadOnlyCollection ids, At } /// - public QueryLayer ComposeForUpdate(TId id, ResourceContext primaryResource) + public QueryLayer ComposeForUpdate(TId id, ResourceType primaryResource) { ArgumentGuard.NotNull(primaryResource, nameof(primaryResource)); @@ -365,15 +361,14 @@ public QueryLayer ComposeForGetRelationshipRightIds(RelationshipAttribute relati ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); - ResourceContext rightResourceContext = _resourceGraph.GetResourceContext(relationship.RightType); - AttrAttribute rightIdAttribute = GetIdAttribute(rightResourceContext); + AttrAttribute rightIdAttribute = GetIdAttribute(relationship.RightType); object[] typedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); - FilterExpression baseFilter = GetFilter(Array.Empty(), rightResourceContext); + FilterExpression baseFilter = GetFilter(Array.Empty(), relationship.RightType); FilterExpression filter = CreateFilterByIds(typedIds, rightIdAttribute, baseFilter); - return new QueryLayer(rightResourceContext) + return new QueryLayer(relationship.RightType) { Include = IncludeExpression.Empty, Filter = filter, @@ -390,23 +385,20 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T ArgumentGuard.NotNull(hasManyRelationship, nameof(hasManyRelationship)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); - ResourceContext leftResourceContext = _resourceGraph.GetResourceContext(hasManyRelationship.LeftType); - AttrAttribute leftIdAttribute = GetIdAttribute(leftResourceContext); - - ResourceContext rightResourceContext = _resourceGraph.GetResourceContext(hasManyRelationship.RightType); - AttrAttribute rightIdAttribute = GetIdAttribute(rightResourceContext); + AttrAttribute leftIdAttribute = GetIdAttribute(hasManyRelationship.LeftType); + AttrAttribute rightIdAttribute = GetIdAttribute(hasManyRelationship.RightType); object[] rightTypedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); FilterExpression leftFilter = CreateFilterByIds(leftId.AsArray(), leftIdAttribute, null); FilterExpression rightFilter = CreateFilterByIds(rightTypedIds, rightIdAttribute, null); - return new QueryLayer(leftResourceContext) + return new QueryLayer(hasManyRelationship.LeftType) { Include = new IncludeExpression(ImmutableHashSet.Create(new IncludeElementExpression(hasManyRelationship))), Filter = leftFilter, Projection = new Dictionary { - [hasManyRelationship] = new(rightResourceContext) + [hasManyRelationship] = new(hasManyRelationship.RightType) { Filter = rightFilter, Projection = new Dictionary @@ -420,36 +412,36 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T } protected virtual IImmutableSet GetIncludeElements(IImmutableSet includeElements, - ResourceContext resourceContext) + ResourceType resourceType) { - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - return _resourceDefinitionAccessor.OnApplyIncludes(resourceContext.ResourceType, includeElements); + return _resourceDefinitionAccessor.OnApplyIncludes(resourceType, includeElements); } - protected virtual FilterExpression GetFilter(IReadOnlyCollection expressionsInScope, ResourceContext resourceContext) + protected virtual FilterExpression GetFilter(IReadOnlyCollection expressionsInScope, ResourceType resourceType) { ArgumentGuard.NotNull(expressionsInScope, nameof(expressionsInScope)); - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); ImmutableArray filters = expressionsInScope.OfType().ToImmutableArray(); FilterExpression filter = filters.Length > 1 ? new LogicalExpression(LogicalOperator.And, filters) : filters.FirstOrDefault(); - return _resourceDefinitionAccessor.OnApplyFilter(resourceContext.ResourceType, filter); + return _resourceDefinitionAccessor.OnApplyFilter(resourceType, filter); } - protected virtual SortExpression GetSort(IReadOnlyCollection expressionsInScope, ResourceContext resourceContext) + protected virtual SortExpression GetSort(IReadOnlyCollection expressionsInScope, ResourceType resourceType) { ArgumentGuard.NotNull(expressionsInScope, nameof(expressionsInScope)); - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); SortExpression sort = expressionsInScope.OfType().FirstOrDefault(); - sort = _resourceDefinitionAccessor.OnApplySort(resourceContext.ResourceType, sort); + sort = _resourceDefinitionAccessor.OnApplySort(resourceType, sort); if (sort == null) { - AttrAttribute idAttribute = GetIdAttribute(resourceContext); + AttrAttribute idAttribute = GetIdAttribute(resourceType); var idAscendingSort = new SortElementExpression(new ResourceFieldChainExpression(idAttribute), true); sort = new SortExpression(ImmutableArray.Create(idAscendingSort)); } @@ -457,25 +449,25 @@ protected virtual SortExpression GetSort(IReadOnlyCollection ex return sort; } - protected virtual PaginationExpression GetPagination(IReadOnlyCollection expressionsInScope, ResourceContext resourceContext) + protected virtual PaginationExpression GetPagination(IReadOnlyCollection expressionsInScope, ResourceType resourceType) { ArgumentGuard.NotNull(expressionsInScope, nameof(expressionsInScope)); - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); PaginationExpression pagination = expressionsInScope.OfType().FirstOrDefault(); - pagination = _resourceDefinitionAccessor.OnApplyPagination(resourceContext.ResourceType, pagination); + pagination = _resourceDefinitionAccessor.OnApplyPagination(resourceType, pagination); pagination ??= new PaginationExpression(PageNumber.ValueOne, _options.DefaultPageSize); return pagination; } - protected virtual IDictionary GetProjectionForSparseAttributeSet(ResourceContext resourceContext) + protected virtual IDictionary GetProjectionForSparseAttributeSet(ResourceType resourceType) { - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForQuery(resourceContext); + IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForQuery(resourceType); if (!fieldSet.Any()) { @@ -483,15 +475,15 @@ protected virtual IDictionary GetProjectionF } HashSet attributeSet = fieldSet.OfType().ToHashSet(); - AttrAttribute idAttribute = GetIdAttribute(resourceContext); + AttrAttribute idAttribute = GetIdAttribute(resourceType); attributeSet.Add(idAttribute); return attributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer)null); } - private static AttrAttribute GetIdAttribute(ResourceContext resourceContext) + private static AttrAttribute GetIdAttribute(ResourceType resourceType) { - return resourceContext.GetAttributeByPropertyName(nameof(Identifiable.Id)); + return resourceType.GetAttributeByPropertyName(nameof(Identifiable.Id)); } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs index 081cf0be34..4dce446d7c 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs @@ -18,19 +18,16 @@ public class IncludeClauseBuilder : QueryClauseBuilder private static readonly IncludeChainConverter IncludeChainConverter = new(); private readonly Expression _source; - private readonly ResourceContext _resourceContext; - private readonly IResourceGraph _resourceGraph; + private readonly ResourceType _resourceType; - public IncludeClauseBuilder(Expression source, LambdaScope lambdaScope, ResourceContext resourceContext, IResourceGraph resourceGraph) + public IncludeClauseBuilder(Expression source, LambdaScope lambdaScope, ResourceType resourceType) : base(lambdaScope) { ArgumentGuard.NotNull(source, nameof(source)); - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); _source = source; - _resourceContext = resourceContext; - _resourceGraph = resourceGraph; + _resourceType = resourceType; } public Expression ApplyInclude(IncludeExpression include) @@ -42,7 +39,7 @@ public Expression ApplyInclude(IncludeExpression include) public override Expression VisitInclude(IncludeExpression expression, object argument) { - Expression source = ApplyEagerLoads(_source, _resourceContext.EagerLoads, null); + Expression source = ApplyEagerLoads(_source, _resourceType.EagerLoads, null); foreach (ResourceFieldChainExpression chain in IncludeChainConverter.GetRelationshipChains(expression)) { @@ -61,8 +58,7 @@ private Expression ProcessRelationshipChain(ResourceFieldChainExpression chain, { path = path == null ? relationship.Property.Name : $"{path}.{relationship.Property.Name}"; - ResourceContext resourceContext = _resourceGraph.GetResourceContext(relationship.RightType); - result = ApplyEagerLoads(result, resourceContext.EagerLoads, path); + result = ApplyEagerLoads(result, relationship.RightType.EagerLoads, path); } return IncludeExtensionMethodCall(result, path); diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs index b32c4246d3..d1412eee39 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs @@ -21,19 +21,17 @@ public class QueryableBuilder private readonly Type _extensionType; private readonly LambdaParameterNameFactory _nameFactory; private readonly IResourceFactory _resourceFactory; - private readonly IResourceGraph _resourceGraph; private readonly IModel _entityModel; private readonly LambdaScopeFactory _lambdaScopeFactory; public QueryableBuilder(Expression source, Type elementType, Type extensionType, LambdaParameterNameFactory nameFactory, - IResourceFactory resourceFactory, IResourceGraph resourceGraph, IModel entityModel, LambdaScopeFactory lambdaScopeFactory = null) + IResourceFactory resourceFactory, IModel entityModel, LambdaScopeFactory lambdaScopeFactory = null) { ArgumentGuard.NotNull(source, nameof(source)); ArgumentGuard.NotNull(elementType, nameof(elementType)); ArgumentGuard.NotNull(extensionType, nameof(extensionType)); ArgumentGuard.NotNull(nameFactory, nameof(nameFactory)); ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(entityModel, nameof(entityModel)); _source = source; @@ -41,7 +39,6 @@ public QueryableBuilder(Expression source, Type elementType, Type extensionType, _extensionType = extensionType; _nameFactory = nameFactory; _resourceFactory = resourceFactory; - _resourceGraph = resourceGraph; _entityModel = entityModel; _lambdaScopeFactory = lambdaScopeFactory ?? new LambdaScopeFactory(_nameFactory); } @@ -54,7 +51,7 @@ public virtual Expression ApplyQuery(QueryLayer layer) if (layer.Include != null) { - expression = ApplyInclude(expression, layer.Include, layer.ResourceContext); + expression = ApplyInclude(expression, layer.Include, layer.ResourceType); } if (layer.Filter != null) @@ -74,17 +71,17 @@ public virtual Expression ApplyQuery(QueryLayer layer) if (!layer.Projection.IsNullOrEmpty()) { - expression = ApplyProjection(expression, layer.Projection, layer.ResourceContext); + expression = ApplyProjection(expression, layer.Projection, layer.ResourceType); } return expression; } - protected virtual Expression ApplyInclude(Expression source, IncludeExpression include, ResourceContext resourceContext) + protected virtual Expression ApplyInclude(Expression source, IncludeExpression include, ResourceType resourceType) { using LambdaScope lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); - var builder = new IncludeClauseBuilder(source, lambdaScope, resourceContext, _resourceGraph); + var builder = new IncludeClauseBuilder(source, lambdaScope, resourceType); return builder.ApplyInclude(include); } @@ -112,13 +109,12 @@ protected virtual Expression ApplyPagination(Expression source, PaginationExpres return builder.ApplySkipTake(pagination); } - protected virtual Expression ApplyProjection(Expression source, IDictionary projection, - ResourceContext resourceContext) + protected virtual Expression ApplyProjection(Expression source, IDictionary projection, ResourceType resourceType) { using LambdaScope lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); - var builder = new SelectClauseBuilder(source, lambdaScope, _entityModel, _extensionType, _nameFactory, _resourceFactory, _resourceGraph); - return builder.ApplySelect(projection, resourceContext); + var builder = new SelectClauseBuilder(source, lambdaScope, _entityModel, _extensionType, _nameFactory, _resourceFactory); + return builder.ApplySelect(projection, resourceType); } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs index f37b2f329e..2fc9c773fc 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs @@ -28,10 +28,9 @@ public class SelectClauseBuilder : QueryClauseBuilder private readonly Type _extensionType; private readonly LambdaParameterNameFactory _nameFactory; private readonly IResourceFactory _resourceFactory; - private readonly IResourceGraph _resourceGraph; public SelectClauseBuilder(Expression source, LambdaScope lambdaScope, IModel entityModel, Type extensionType, LambdaParameterNameFactory nameFactory, - IResourceFactory resourceFactory, IResourceGraph resourceGraph) + IResourceFactory resourceFactory) : base(lambdaScope) { ArgumentGuard.NotNull(source, nameof(source)); @@ -39,17 +38,15 @@ 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(resourceGraph, nameof(resourceGraph)); _source = source; _entityModel = entityModel; _extensionType = extensionType; _nameFactory = nameFactory; _resourceFactory = resourceFactory; - _resourceGraph = resourceGraph; } - public Expression ApplySelect(IDictionary selectors, ResourceContext resourceContext) + public Expression ApplySelect(IDictionary selectors, ResourceType resourceType) { ArgumentGuard.NotNull(selectors, nameof(selectors)); @@ -58,17 +55,17 @@ public Expression ApplySelect(IDictionary se return _source; } - Expression bodyInitializer = CreateLambdaBodyInitializer(selectors, resourceContext, LambdaScope, false); + Expression bodyInitializer = CreateLambdaBodyInitializer(selectors, resourceType, LambdaScope, false); LambdaExpression lambda = Expression.Lambda(bodyInitializer, LambdaScope.Parameter); return SelectExtensionMethodCall(_source, LambdaScope.Parameter.Type, lambda); } - private Expression CreateLambdaBodyInitializer(IDictionary selectors, ResourceContext resourceContext, + private Expression CreateLambdaBodyInitializer(IDictionary selectors, ResourceType resourceType, LambdaScope lambdaScope, bool lambdaAccessorRequiresTestForNull) { - ICollection propertySelectors = ToPropertySelectors(selectors, resourceContext, lambdaScope.Accessor.Type); + ICollection propertySelectors = ToPropertySelectors(selectors, resourceType, lambdaScope.Accessor.Type); MemberBinding[] propertyAssignments = propertySelectors.Select(selector => CreatePropertyAssignment(selector, lambdaScope)).Cast().ToArray(); @@ -85,7 +82,7 @@ private Expression CreateLambdaBodyInitializer(IDictionary ToPropertySelectors(IDictionary resourceFieldSelectors, - ResourceContext resourceContext, Type elementType) + ResourceType resourceType, Type elementType) { var propertySelectors = new Dictionary(); @@ -121,7 +118,7 @@ private ICollection ToPropertySelectors(IDictionary>> _lazySourceTable; - private readonly IDictionary> _visitedTable; + private readonly Lazy>> _lazySourceTable; + private readonly IDictionary> _visitedTable; public SparseFieldSetCache(IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor) { @@ -22,17 +22,17 @@ public SparseFieldSetCache(IEnumerable constraintProvi ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); _resourceDefinitionAccessor = resourceDefinitionAccessor; - _lazySourceTable = new Lazy>>(() => BuildSourceTable(constraintProviders)); - _visitedTable = new Dictionary>(); + _lazySourceTable = new Lazy>>(() => BuildSourceTable(constraintProviders)); + _visitedTable = new Dictionary>(); } - private static IDictionary> BuildSourceTable( + private static IDictionary> BuildSourceTable( IEnumerable constraintProviders) { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - KeyValuePair[] sparseFieldTables = constraintProviders + KeyValuePair[] sparseFieldTables = constraintProviders .SelectMany(provider => provider.GetConstraints()) .Where(constraint => constraint.Scope == null) .Select(constraint => constraint.Expression) @@ -44,16 +44,16 @@ private static IDictionary.Builder>(); + var mergedTable = new Dictionary.Builder>(); - foreach ((ResourceContext resourceContext, SparseFieldSetExpression sparseFieldSet) in sparseFieldTables) + foreach ((ResourceType resourceType, SparseFieldSetExpression sparseFieldSet) in sparseFieldTables) { - if (!mergedTable.ContainsKey(resourceContext)) + if (!mergedTable.ContainsKey(resourceType)) { - mergedTable[resourceContext] = ImmutableHashSet.CreateBuilder(); + mergedTable[resourceType] = ImmutableHashSet.CreateBuilder(); } - AddSparseFieldsToSet(sparseFieldSet.Fields, mergedTable[resourceContext]); + AddSparseFieldsToSet(sparseFieldSet.Fields, mergedTable[resourceType]); } return mergedTable.ToDictionary(pair => pair.Key, pair => (IImmutableSet)pair.Value.ToImmutable()); @@ -69,38 +69,38 @@ private static void AddSparseFieldsToSet(IImmutableSet s } /// - public IImmutableSet GetSparseFieldSetForQuery(ResourceContext resourceContext) + public IImmutableSet GetSparseFieldSetForQuery(ResourceType resourceType) { - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - if (!_visitedTable.ContainsKey(resourceContext)) + if (!_visitedTable.ContainsKey(resourceType)) { - SparseFieldSetExpression inputExpression = _lazySourceTable.Value.ContainsKey(resourceContext) - ? new SparseFieldSetExpression(_lazySourceTable.Value[resourceContext]) + SparseFieldSetExpression inputExpression = _lazySourceTable.Value.ContainsKey(resourceType) + ? new SparseFieldSetExpression(_lazySourceTable.Value[resourceType]) : null; - SparseFieldSetExpression outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); + SparseFieldSetExpression outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceType, inputExpression); IImmutableSet outputFields = outputExpression == null ? ImmutableHashSet.Empty : outputExpression.Fields; - _visitedTable[resourceContext] = outputFields; + _visitedTable[resourceType] = outputFields; } - return _visitedTable[resourceContext]; + return _visitedTable[resourceType]; } /// - public IImmutableSet GetIdAttributeSetForRelationshipQuery(ResourceContext resourceContext) + public IImmutableSet GetIdAttributeSetForRelationshipQuery(ResourceType resourceType) { - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - AttrAttribute idAttribute = resourceContext.GetAttributeByPropertyName(nameof(Identifiable.Id)); + AttrAttribute idAttribute = resourceType.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). - SparseFieldSetExpression outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); + SparseFieldSetExpression outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceType, inputExpression); ImmutableHashSet outputAttributes = outputExpression == null ? ImmutableHashSet.Empty @@ -111,40 +111,40 @@ public IImmutableSet GetIdAttributeSetForRelationshipQuery(Resour } /// - public IImmutableSet GetSparseFieldSetForSerializer(ResourceContext resourceContext) + public IImmutableSet GetSparseFieldSetForSerializer(ResourceType resourceType) { - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - if (!_visitedTable.ContainsKey(resourceContext)) + if (!_visitedTable.ContainsKey(resourceType)) { - IImmutableSet inputFields = _lazySourceTable.Value.ContainsKey(resourceContext) - ? _lazySourceTable.Value[resourceContext] - : GetResourceFields(resourceContext); + IImmutableSet inputFields = _lazySourceTable.Value.ContainsKey(resourceType) + ? _lazySourceTable.Value[resourceType] + : GetResourceFields(resourceType); var inputExpression = new SparseFieldSetExpression(inputFields); - SparseFieldSetExpression outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); + SparseFieldSetExpression outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceType, inputExpression); IImmutableSet outputFields = - outputExpression == null ? GetResourceFields(resourceContext) : inputFields.Intersect(outputExpression.Fields); + outputExpression == null ? GetResourceFields(resourceType) : inputFields.Intersect(outputExpression.Fields); - _visitedTable[resourceContext] = outputFields; + _visitedTable[resourceType] = outputFields; } - return _visitedTable[resourceContext]; + return _visitedTable[resourceType]; } - private IImmutableSet GetResourceFields(ResourceContext resourceContext) + private IImmutableSet GetResourceFields(ResourceType resourceType) { - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); ImmutableHashSet.Builder fieldSetBuilder = ImmutableHashSet.CreateBuilder(); - foreach (AttrAttribute attribute in resourceContext.Attributes.Where(attr => attr.Capabilities.HasFlag(AttrCapabilities.AllowView))) + foreach (AttrAttribute attribute in resourceType.Attributes.Where(attr => attr.Capabilities.HasFlag(AttrCapabilities.AllowView))) { fieldSetBuilder.Add(attribute); } - fieldSetBuilder.AddRange(resourceContext.Relationships); + fieldSetBuilder.AddRange(resourceType.Relationships); return fieldSetBuilder.ToImmutable(); } diff --git a/src/JsonApiDotNetCore/Queries/QueryLayer.cs b/src/JsonApiDotNetCore/Queries/QueryLayer.cs index 9340181743..e8fa0e6cfa 100644 --- a/src/JsonApiDotNetCore/Queries/QueryLayer.cs +++ b/src/JsonApiDotNetCore/Queries/QueryLayer.cs @@ -14,7 +14,7 @@ namespace JsonApiDotNetCore.Queries [PublicAPI] public sealed class QueryLayer { - public ResourceContext ResourceContext { get; } + public ResourceType ResourceType { get; } public IncludeExpression Include { get; set; } public FilterExpression Filter { get; set; } @@ -22,11 +22,11 @@ public sealed class QueryLayer public PaginationExpression Pagination { get; set; } public IDictionary Projection { get; set; } - public QueryLayer(ResourceContext resourceContext) + public QueryLayer(ResourceType resourceType) { - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - ResourceContext = resourceContext; + ResourceType = resourceType; } public override string ToString() @@ -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.ResourceType.ClrType.Name}>"); using (writer.Indent()) { diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs index 354cb4b8ec..9cf8ede3dc 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs @@ -36,11 +36,11 @@ public FilterQueryStringParameterReader(IJsonApiRequest request, IResourceGraph ArgumentGuard.NotNull(options, nameof(options)); _options = options; - _scopeParser = new QueryStringParameterScopeParser(resourceGraph, FieldChainRequirements.EndsInToMany); - _filterParser = new FilterParser(resourceGraph, resourceFactory, ValidateSingleField); + _scopeParser = new QueryStringParameterScopeParser(FieldChainRequirements.EndsInToMany); + _filterParser = new FilterParser(resourceFactory, ValidateSingleField); } - protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) + protected void ValidateSingleField(ResourceFieldAttribute field, ResourceType resourceType, string path) { if (field is AttrAttribute attribute && !attribute.Capabilities.HasFlag(AttrCapabilities.AllowFilter)) { @@ -117,7 +117,7 @@ private void ReadSingleValue(string parameterName, string parameterValue) private ResourceFieldChainExpression GetScope(string parameterName) { - QueryStringParameterScopeExpression parameterScope = _scopeParser.Parse(parameterName, RequestResource); + QueryStringParameterScopeExpression parameterScope = _scopeParser.Parse(parameterName, RequestResourceType); if (parameterScope.Scope == null) { @@ -129,8 +129,8 @@ private ResourceFieldChainExpression GetScope(string parameterName) private FilterExpression GetFilter(string parameterValue, ResourceFieldChainExpression scope) { - ResourceContext resourceContextInScope = GetResourceContextForScope(scope); - return _filterParser.Parse(parameterValue, resourceContextInScope); + ResourceType resourceTypeInScope = GetResourceTypeForScope(scope); + return _filterParser.Parse(parameterValue, resourceTypeInScope); } private void StoreFilterInScope(FilterExpression filter, ResourceFieldChainExpression scope) diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs index 2bed425170..9bdf16851a 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs @@ -27,17 +27,17 @@ public IncludeQueryStringParameterReader(IJsonApiRequest request, IResourceGraph ArgumentGuard.NotNull(options, nameof(options)); _options = options; - _includeParser = new IncludeParser(resourceGraph, ValidateSingleRelationship); + _includeParser = new IncludeParser(ValidateSingleRelationship); } - protected void ValidateSingleRelationship(RelationshipAttribute relationship, ResourceContext resourceContext, string path) + protected void ValidateSingleRelationship(RelationshipAttribute relationship, ResourceType resourceType, string path) { if (!relationship.CanInclude) { throw new InvalidQueryStringParameterException(_lastParameterName, "Including the requested relationship is not allowed.", path == relationship.PublicName - ? $"Including the relationship '{relationship.PublicName}' on '{resourceContext.PublicName}' is not allowed." - : $"Including the relationship '{relationship.PublicName}' in '{path}' on '{resourceContext.PublicName}' is not allowed."); + ? $"Including the relationship '{relationship.PublicName}' on '{resourceType.PublicName}' is not allowed." + : $"Including the relationship '{relationship.PublicName}' in '{path}' on '{resourceType.PublicName}' is not allowed."); } } @@ -72,7 +72,7 @@ public virtual void Read(string parameterName, StringValues parameterValue) private IncludeExpression GetInclude(string parameterValue) { - return _includeParser.Parse(parameterValue, RequestResource, _options.MaximumIncludeDepth); + return _includeParser.Parse(parameterValue, RequestResourceType, _options.MaximumIncludeDepth); } /// diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs index aa117847ea..e54d9617b0 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs @@ -31,7 +31,7 @@ public PaginationQueryStringParameterReader(IJsonApiRequest request, IResourceGr ArgumentGuard.NotNull(options, nameof(options)); _options = options; - _paginationParser = new PaginationParser(resourceGraph); + _paginationParser = new PaginationParser(); } /// @@ -79,7 +79,7 @@ public virtual void Read(string parameterName, StringValues parameterValue) private PaginationQueryStringValueExpression GetPageConstraint(string parameterValue) { - return _paginationParser.Parse(parameterValue, RequestResource); + return _paginationParser.Parse(parameterValue, RequestResourceType); } protected virtual void ValidatePageSize(PaginationQueryStringValueExpression constraint) @@ -120,12 +120,12 @@ protected virtual void ValidatePageNumber(PaginationQueryStringValueExpression c /// public virtual IReadOnlyCollection GetConstraints() { - var context = new PaginationContext(); + var paginationState = new PaginationState(); foreach (PaginationElementQueryStringValueExpression element in _pageSizeConstraint?.Elements ?? ImmutableArray.Empty) { - MutablePaginationEntry entry = context.ResolveEntryInScope(element.Scope); + MutablePaginationEntry entry = paginationState.ResolveEntryInScope(element.Scope); entry.PageSize = element.Value == 0 ? null : new PageSize(element.Value); entry.HasSetPageSize = true; } @@ -133,16 +133,16 @@ public virtual IReadOnlyCollection GetConstraints() foreach (PaginationElementQueryStringValueExpression element in _pageNumberConstraint?.Elements ?? ImmutableArray.Empty) { - MutablePaginationEntry entry = context.ResolveEntryInScope(element.Scope); + MutablePaginationEntry entry = paginationState.ResolveEntryInScope(element.Scope); entry.PageNumber = new PageNumber(element.Value); } - context.ApplyOptions(_options); + paginationState.ApplyOptions(_options); - return context.GetExpressionsInScope(); + return paginationState.GetExpressionsInScope(); } - private sealed class PaginationContext + private sealed class PaginationState { private readonly MutablePaginationEntry _globalScope = new(); private readonly Dictionary _nestedScopes = new(); diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs index b026ae7587..615bf8ced8 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs @@ -1,4 +1,3 @@ -using System; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries.Expressions; @@ -12,7 +11,7 @@ public abstract class QueryStringParameterReader private readonly IResourceGraph _resourceGraph; private readonly bool _isCollectionRequest; - protected ResourceContext RequestResource { get; } + protected ResourceType RequestResourceType { get; } protected bool IsAtomicOperationsRequest { get; } protected QueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph) @@ -22,21 +21,25 @@ protected QueryStringParameterReader(IJsonApiRequest request, IResourceGraph res _resourceGraph = resourceGraph; _isCollectionRequest = request.IsCollection; - RequestResource = request.SecondaryResource ?? request.PrimaryResource; + RequestResourceType = request.SecondaryResourceType ?? request.PrimaryResourceType; IsAtomicOperationsRequest = request.Kind == EndpointKind.AtomicOperations; } - protected ResourceContext GetResourceContextForScope(ResourceFieldChainExpression scope) + protected ResourceType GetResourceTypeForScope(ResourceFieldChainExpression scope) { if (scope == null) { - return RequestResource; + return RequestResourceType; } ResourceFieldAttribute lastField = scope.Fields[^1]; - Type type = lastField is RelationshipAttribute relationship ? relationship.RightType : lastField.Property.PropertyType; - return _resourceGraph.GetResourceContext(type); + if (lastField is RelationshipAttribute relationship) + { + return relationship.RightType; + } + + return _resourceGraph.GetResourceType(lastField.Property.PropertyType); } protected void AssertIsCollectionRequest() diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs index 5a3e1cab3c..98994c4c6c 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs @@ -56,8 +56,8 @@ public virtual void Read(string parameterName, StringValues parameterValue) private object GetQueryableHandler(string parameterName) { - Type resourceType = (_request.SecondaryResource ?? _request.PrimaryResource).ResourceType; - object handler = _resourceDefinitionAccessor.GetQueryableHandlerForQueryStringParameter(resourceType, parameterName); + Type resourceClrType = (_request.SecondaryResourceType ?? _request.PrimaryResourceType).ClrType; + object handler = _resourceDefinitionAccessor.GetQueryableHandlerForQueryStringParameter(resourceClrType, parameterName); if (handler != null && _request.Kind != EndpointKind.Primary) { diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs index e1ca5e0cd8..c9583b5edf 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs @@ -24,11 +24,11 @@ public class SortQueryStringParameterReader : QueryStringParameterReader, ISortQ public SortQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph) : base(request, resourceGraph) { - _scopeParser = new QueryStringParameterScopeParser(resourceGraph, FieldChainRequirements.EndsInToMany); - _sortParser = new SortParser(resourceGraph, ValidateSingleField); + _scopeParser = new QueryStringParameterScopeParser(FieldChainRequirements.EndsInToMany); + _sortParser = new SortParser(ValidateSingleField); } - protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) + protected void ValidateSingleField(ResourceFieldAttribute field, ResourceType resourceType, string path) { if (field is AttrAttribute attribute && !attribute.Capabilities.HasFlag(AttrCapabilities.AllowSort)) { @@ -75,7 +75,7 @@ public virtual void Read(string parameterName, StringValues parameterValue) private ResourceFieldChainExpression GetScope(string parameterName) { - QueryStringParameterScopeExpression parameterScope = _scopeParser.Parse(parameterName, RequestResource); + QueryStringParameterScopeExpression parameterScope = _scopeParser.Parse(parameterName, RequestResourceType); if (parameterScope.Scope == null) { @@ -87,8 +87,8 @@ private ResourceFieldChainExpression GetScope(string parameterName) private SortExpression GetSort(string parameterValue, ResourceFieldChainExpression scope) { - ResourceContext resourceContextInScope = GetResourceContextForScope(scope); - return _sortParser.Parse(parameterValue, resourceContextInScope); + ResourceType resourceTypeInScope = GetResourceTypeForScope(scope); + return _sortParser.Parse(parameterValue, resourceTypeInScope); } /// diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs index 096b31a7a1..2872351bd9 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs @@ -22,8 +22,8 @@ public class SparseFieldSetQueryStringParameterReader : QueryStringParameterRead private readonly SparseFieldTypeParser _sparseFieldTypeParser; private readonly SparseFieldSetParser _sparseFieldSetParser; - private readonly ImmutableDictionary.Builder _sparseFieldTableBuilder = - ImmutableDictionary.CreateBuilder(); + private readonly ImmutableDictionary.Builder _sparseFieldTableBuilder = + ImmutableDictionary.CreateBuilder(); private string _lastParameterName; @@ -34,10 +34,10 @@ public SparseFieldSetQueryStringParameterReader(IJsonApiRequest request, IResour : base(request, resourceGraph) { _sparseFieldTypeParser = new SparseFieldTypeParser(resourceGraph); - _sparseFieldSetParser = new SparseFieldSetParser(resourceGraph, ValidateSingleField); + _sparseFieldSetParser = new SparseFieldSetParser(ValidateSingleField); } - protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) + protected void ValidateSingleField(ResourceFieldAttribute field, ResourceType resourceType, string path) { if (field is AttrAttribute attribute && !attribute.Capabilities.HasFlag(AttrCapabilities.AllowView)) { @@ -69,7 +69,7 @@ public virtual void Read(string parameterName, StringValues parameterValue) try { - ResourceContext targetResource = GetSparseFieldType(parameterName); + ResourceType targetResource = GetSparseFieldType(parameterName); SparseFieldSetExpression sparseFieldSet = GetSparseFieldSet(parameterValue, targetResource); _sparseFieldTableBuilder[targetResource] = sparseFieldSet; @@ -80,19 +80,19 @@ public virtual void Read(string parameterName, StringValues parameterValue) } } - private ResourceContext GetSparseFieldType(string parameterName) + private ResourceType GetSparseFieldType(string parameterName) { return _sparseFieldTypeParser.Parse(parameterName); } - private SparseFieldSetExpression GetSparseFieldSet(string parameterValue, ResourceContext resourceContext) + private SparseFieldSetExpression GetSparseFieldSet(string parameterValue, ResourceType resourceType) { - SparseFieldSetExpression sparseFieldSet = _sparseFieldSetParser.Parse(parameterValue, resourceContext); + SparseFieldSetExpression sparseFieldSet = _sparseFieldSetParser.Parse(parameterValue, resourceType); 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.GetAttributeByPropertyName(nameof(Identifiable.Id)); + AttrAttribute idAttribute = resourceType.GetAttributeByPropertyName(nameof(Identifiable.Id)); return new SparseFieldSetExpression(ImmutableHashSet.Create(idAttribute)); } diff --git a/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs b/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs index 4610beb0e6..f80bb3833c 100644 --- a/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs +++ b/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs @@ -37,17 +37,17 @@ public static object GetTrackedIdentifiable(this DbContext dbContext, IIdentifia ArgumentGuard.NotNull(dbContext, nameof(dbContext)); ArgumentGuard.NotNull(identifiable, nameof(identifiable)); - Type resourceType = identifiable.GetType(); + Type resourceClrType = identifiable.GetType(); string stringId = identifiable.StringId; - EntityEntry entityEntry = dbContext.ChangeTracker.Entries().FirstOrDefault(entry => IsResource(entry, resourceType, stringId)); + EntityEntry entityEntry = dbContext.ChangeTracker.Entries().FirstOrDefault(entry => IsResource(entry, resourceClrType, stringId)); return entityEntry?.Entity; } - private static bool IsResource(EntityEntry entry, Type resourceType, string stringId) + private static bool IsResource(EntityEntry entry, Type resourceClrType, string stringId) { - return entry.Entity.GetType() == resourceType && ((IIdentifiable)entry.Entity).StringId == stringId; + return entry.Entity.GetType() == resourceClrType && ((IIdentifiable)entry.Entity).StringId == stringId; } /// diff --git a/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs b/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs index 4e1c7b4552..61dfcb388d 100644 --- a/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs +++ b/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs @@ -8,23 +8,23 @@ namespace JsonApiDotNetCore.Repositories public sealed class DbContextResolver : IDbContextResolver where TDbContext : DbContext { - private readonly TDbContext _context; + private readonly TDbContext _dbContext; - public DbContextResolver(TDbContext context) + public DbContextResolver(TDbContext dbContext) { - ArgumentGuard.NotNull(context, nameof(context)); + ArgumentGuard.NotNull(dbContext, nameof(dbContext)); - _context = context; + _dbContext = dbContext; } public DbContext GetContext() { - return _context; + return _dbContext; } public TDbContext GetTypedContext() { - return _context; + return _dbContext; } } } diff --git a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs index a929856fa1..802d9cb100 100644 --- a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs @@ -41,12 +41,12 @@ public class EntityFrameworkCoreRepository : IResourceRepository /// public virtual string TransactionId => _dbContext.Database.CurrentTransaction?.TransactionId.ToString(); - public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor) { ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); - ArgumentGuard.NotNull(contextResolver, nameof(contextResolver)); + ArgumentGuard.NotNull(dbContextResolver, nameof(dbContextResolver)); ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); @@ -54,7 +54,7 @@ public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextR ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); _targetedFields = targetedFields; - _dbContext = contextResolver.GetContext(); + _dbContext = dbContextResolver.GetContext(); _resourceGraph = resourceGraph; _resourceFactory = resourceFactory; _constraintProviders = constraintProviders; @@ -93,9 +93,9 @@ public virtual async Task CountAsync(FilterExpression topFilter, Cancellati using (CodeTimingSessionManager.Current.Measure("Repository - Count resources")) { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(); + ResourceType resourceType = _resourceGraph.GetResourceType(); - var layer = new QueryLayer(resourceContext) + var layer = new QueryLayer(resourceType) { Filter = topFilter }; @@ -142,8 +142,7 @@ protected virtual IQueryable ApplyQueryLayer(QueryLayer layer) var nameFactory = new LambdaParameterNameFactory(); - var builder = new QueryableBuilder(source.Expression, source.ElementType, typeof(Queryable), nameFactory, _resourceFactory, _resourceGraph, - _dbContext.Model); + var builder = new QueryableBuilder(source.Expression, source.ElementType, typeof(Queryable), nameFactory, _resourceFactory, _dbContext.Model); Expression expression = builder.ApplyQuery(layer); @@ -297,8 +296,8 @@ protected void AssertIsNotClearingRequiredRelationship(RelationshipAttribute rel if (relationshipIsRequired && relationshipIsBeingCleared) { - string resourceType = _resourceGraph.GetResourceContext().PublicName; - throw new CannotClearRequiredRelationshipException(relationship.PublicName, leftResource.StringId, resourceType); + string resourceName = _resourceGraph.GetResourceType().PublicName; + throw new CannotClearRequiredRelationshipException(relationship.PublicName, leftResource.StringId, resourceName); } } @@ -335,7 +334,7 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke var resourceTracked = (TResource)_dbContext.GetTrackedOrAttach(placeholderResource); - foreach (RelationshipAttribute relationship in _resourceGraph.GetResourceContext().Relationships) + foreach (RelationshipAttribute relationship in _resourceGraph.GetResourceType().Relationships) { // Loads the data of the relationship, if in EF Core it is configured in such a way that loading the related // entities into memory is required for successfully executing the selected deletion behavior. @@ -593,10 +592,10 @@ protected virtual async Task SaveChangesAsync(CancellationToken cancellationToke public class EntityFrameworkCoreRepository : EntityFrameworkCoreRepository, IResourceRepository where TResource : class, IIdentifiable { - public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) + : base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } } diff --git a/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs index 4925697112..bf67a0a480 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs @@ -1,7 +1,7 @@ -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; @@ -22,7 +22,7 @@ Task> GetAsync(QueryLayer layer, Cance /// /// Invokes for the specified resource type. /// - Task> GetAsync(Type resourceType, QueryLayer layer, CancellationToken cancellationToken); + Task> GetAsync(ResourceType resourceType, QueryLayer layer, CancellationToken cancellationToken); /// /// Invokes for the specified resource type. diff --git a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs index d1ccdb418d..1e71016a15 100644 --- a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs @@ -41,7 +41,7 @@ public async Task> GetAsync(QueryLayer } /// - public async Task> GetAsync(Type resourceType, QueryLayer layer, CancellationToken cancellationToken) + public async Task> GetAsync(ResourceType resourceType, QueryLayer layer, CancellationToken cancellationToken) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); @@ -122,13 +122,17 @@ public async Task RemoveFromToManyRelationshipAsync(TResource leftRes await repository.RemoveFromToManyRelationshipAsync(leftResource, rightResourceIds, cancellationToken); } - protected virtual object ResolveReadRepository(Type resourceType) + protected object ResolveReadRepository(Type resourceClrType) { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); + ResourceType resourceType = _resourceGraph.GetResourceType(resourceClrType); + return ResolveReadRepository(resourceType); + } - if (resourceContext.IdentityType == typeof(int)) + protected virtual object ResolveReadRepository(ResourceType resourceType) + { + if (resourceType.IdentityClrType == typeof(int)) { - Type intRepositoryType = typeof(IResourceReadRepository<>).MakeGenericType(resourceContext.ResourceType); + Type intRepositoryType = typeof(IResourceReadRepository<>).MakeGenericType(resourceType.ClrType); object intRepository = _serviceProvider.GetService(intRepositoryType); if (intRepository != null) @@ -137,20 +141,20 @@ protected virtual object ResolveReadRepository(Type resourceType) } } - Type resourceDefinitionType = typeof(IResourceReadRepository<,>).MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); + Type resourceDefinitionType = typeof(IResourceReadRepository<,>).MakeGenericType(resourceType.ClrType, resourceType.IdentityClrType); return _serviceProvider.GetRequiredService(resourceDefinitionType); } - private object GetWriteRepository(Type resourceType) + private object GetWriteRepository(Type resourceClrType) { - object writeRepository = ResolveWriteRepository(resourceType); + object writeRepository = ResolveWriteRepository(resourceClrType); if (_request.TransactionId != null) { if (writeRepository is not IRepositorySupportsTransaction repository) { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); - throw new MissingTransactionSupportException(resourceContext.PublicName); + ResourceType resourceType = _resourceGraph.GetResourceType(resourceClrType); + throw new MissingTransactionSupportException(resourceType.PublicName); } if (repository.TransactionId != _request.TransactionId) @@ -162,13 +166,13 @@ private object GetWriteRepository(Type resourceType) return writeRepository; } - protected virtual object ResolveWriteRepository(Type resourceType) + protected virtual object ResolveWriteRepository(Type resourceClrType) { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); + ResourceType resourceType = _resourceGraph.GetResourceType(resourceClrType); - if (resourceContext.IdentityType == typeof(int)) + if (resourceType.IdentityClrType == typeof(int)) { - Type intRepositoryType = typeof(IResourceWriteRepository<>).MakeGenericType(resourceContext.ResourceType); + Type intRepositoryType = typeof(IResourceWriteRepository<>).MakeGenericType(resourceType.ClrType); object intRepository = _serviceProvider.GetService(intRepositoryType); if (intRepository != null) @@ -177,7 +181,7 @@ protected virtual object ResolveWriteRepository(Type resourceType) } } - Type resourceDefinitionType = typeof(IResourceWriteRepository<,>).MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); + Type resourceDefinitionType = typeof(IResourceWriteRepository<,>).MakeGenericType(resourceType.ClrType, resourceType.IdentityClrType); return _serviceProvider.GetRequiredService(resourceDefinitionType); } } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs index eaeb10360d..732da4c4fc 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs @@ -16,19 +16,36 @@ public abstract class RelationshipAttribute : ResourceFieldAttribute private protected static readonly CollectionConverter CollectionConverter = new(); /// - /// The property name of the EF Core inverse navigation, which may or may not exist. Even if it exists, it may not be exposed as a JSON:API relationship. + /// The CLR type in which this relationship is declared. + /// + internal Type LeftClrType { get; set; } + + /// + /// The CLR type this relationship points to. In the case of a relationship, this value will be the collection element + /// type. + /// + /// + /// Tags { get; set; } // RightClrType: typeof(Tag) + /// ]]> + /// + internal Type RightClrType { get; set; } + + /// + /// The of the EF Core inverse navigation, which may or may not exist. Even if it exists, it may not be exposed as a JSON:API + /// relationship. /// /// /// Articles { get; set; } /// } /// ]]> @@ -36,20 +53,15 @@ public abstract class RelationshipAttribute : ResourceFieldAttribute public PropertyInfo InverseNavigationProperty { get; set; } /// - /// The containing type in which this relationship is declared. + /// The containing resource type in which this relationship is declared. /// - public Type LeftType { get; internal set; } + public ResourceType LeftType { get; internal set; } /// - /// The type this relationship points to. This does not necessarily match the relationship property type. In the case of a - /// relationship, this value will be the collection element type. + /// The resource type this relationship points to. In the case of a relationship, this value will be the collection + /// element type. /// - /// - /// Tags { get; set; } // RightType == typeof(Tag) - /// ]]> - /// - public Type RightType { get; internal set; } + public ResourceType RightType { get; internal set; } /// /// Configures which links to show in the object for this relationship. Defaults to @@ -101,12 +113,13 @@ public override bool Equals(object obj) var other = (RelationshipAttribute)obj; - return LeftType == other.LeftType && RightType == other.RightType && Links == other.Links && CanInclude == other.CanInclude && base.Equals(other); + return LeftClrType == other.LeftClrType && RightClrType == other.RightClrType && Links == other.Links && CanInclude == other.CanInclude && + base.Equals(other); } public override int GetHashCode() { - return HashCode.Combine(LeftType, RightType, Links, CanInclude, base.GetHashCode()); + return HashCode.Combine(LeftClrType, RightClrType, Links, CanInclude, base.GetHashCode()); } } } diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs index d9afc91c24..c4b91365bd 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; @@ -18,38 +19,38 @@ public interface IResourceDefinitionAccessor /// /// Invokes for the specified resource type. /// - IImmutableSet OnApplyIncludes(Type resourceType, IImmutableSet existingIncludes); + IImmutableSet OnApplyIncludes(ResourceType resourceType, IImmutableSet existingIncludes); /// /// Invokes for the specified resource type. /// - FilterExpression OnApplyFilter(Type resourceType, FilterExpression existingFilter); + FilterExpression OnApplyFilter(ResourceType resourceType, FilterExpression existingFilter); /// /// Invokes for the specified resource type. /// - SortExpression OnApplySort(Type resourceType, SortExpression existingSort); + SortExpression OnApplySort(ResourceType resourceType, SortExpression existingSort); /// /// Invokes for the specified resource type. /// - PaginationExpression OnApplyPagination(Type resourceType, PaginationExpression existingPagination); + PaginationExpression OnApplyPagination(ResourceType resourceType, PaginationExpression existingPagination); /// /// Invokes for the specified resource type. /// - SparseFieldSetExpression OnApplySparseFieldSet(Type resourceType, SparseFieldSetExpression existingSparseFieldSet); + SparseFieldSetExpression OnApplySparseFieldSet(ResourceType resourceType, SparseFieldSetExpression existingSparseFieldSet); /// /// Invokes for the specified resource type, then /// returns the expression for the specified parameter name. /// - object GetQueryableHandlerForQueryStringParameter(Type resourceType, string parameterName); + object GetQueryableHandlerForQueryStringParameter(Type resourceClrType, string parameterName); /// /// Invokes for the specified resource. /// - IDictionary GetMeta(Type resourceType, IIdentifiable resourceInstance); + IDictionary GetMeta(ResourceType resourceType, IIdentifiable resourceInstance); /// /// Invokes for the specified resource. diff --git a/src/JsonApiDotNetCore/Resources/IResourceFactory.cs b/src/JsonApiDotNetCore/Resources/IResourceFactory.cs index 38a25ad996..1e37304528 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceFactory.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceFactory.cs @@ -11,7 +11,7 @@ public interface IResourceFactory /// /// Creates a new resource object instance. /// - public IIdentifiable CreateInstance(Type resourceType); + public IIdentifiable CreateInstance(Type resourceClrType); /// /// Creates a new resource object instance. @@ -22,6 +22,6 @@ public TResource CreateInstance() /// /// Returns an expression tree that represents creating a new resource object instance. /// - public NewExpression CreateNewExpression(Type resourceType); + public NewExpression CreateNewExpression(Type resourceClrType); } } diff --git a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs index fa2206f5c4..b4110b28cd 100644 --- a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs @@ -41,14 +41,14 @@ public class JsonApiResourceDefinition : IResourceDefinition /// Provides metadata for the resource type . /// - protected ResourceContext ResourceContext { get; } + protected ResourceType ResourceType { get; } public JsonApiResourceDefinition(IResourceGraph resourceGraph) { ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ResourceGraph = resourceGraph; - ResourceContext = resourceGraph.GetResourceContext(); + ResourceType = resourceGraph.GetResourceType(); } /// diff --git a/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs index b29fe33ef1..1158df6183 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Resources public sealed class ResourceChangeTracker : IResourceChangeTracker where TResource : class, IIdentifiable { - private readonly IResourceGraph _resourceGraph; + private readonly ResourceType _resourceType; private readonly ITargetedFields _targetedFields; private IDictionary _initiallyStoredAttributeValues; @@ -23,7 +23,7 @@ public ResourceChangeTracker(IResourceGraph resourceGraph, ITargetedFields targe ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); - _resourceGraph = resourceGraph; + _resourceType = resourceGraph.GetResourceType(); _targetedFields = targetedFields; } @@ -32,8 +32,7 @@ public void SetInitiallyStoredAttributeValues(TResource resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - ResourceContext resourceContext = _resourceGraph.GetResourceContext(); - _initiallyStoredAttributeValues = CreateAttributeDictionary(resource, resourceContext.Attributes); + _initiallyStoredAttributeValues = CreateAttributeDictionary(resource, _resourceType.Attributes); } /// @@ -49,8 +48,7 @@ public void SetFinallyStoredAttributeValues(TResource resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - ResourceContext resourceContext = _resourceGraph.GetResourceContext(); - _finallyStoredAttributeValues = CreateAttributeDictionary(resource, resourceContext.Attributes); + _finallyStoredAttributeValues = CreateAttributeDictionary(resource, _resourceType.Attributes); } private IDictionary CreateAttributeDictionary(TResource resource, IEnumerable attributes) diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs index fd0656e2bd..36e8f008d3 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -29,7 +29,7 @@ public ResourceDefinitionAccessor(IResourceGraph resourceGraph, IServiceProvider } /// - public IImmutableSet OnApplyIncludes(Type resourceType, IImmutableSet existingIncludes) + public IImmutableSet OnApplyIncludes(ResourceType resourceType, IImmutableSet existingIncludes) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); @@ -38,7 +38,7 @@ public IImmutableSet OnApplyIncludes(Type resourceType } /// - public FilterExpression OnApplyFilter(Type resourceType, FilterExpression existingFilter) + public FilterExpression OnApplyFilter(ResourceType resourceType, FilterExpression existingFilter) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); @@ -47,7 +47,7 @@ public FilterExpression OnApplyFilter(Type resourceType, FilterExpression existi } /// - public SortExpression OnApplySort(Type resourceType, SortExpression existingSort) + public SortExpression OnApplySort(ResourceType resourceType, SortExpression existingSort) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); @@ -56,7 +56,7 @@ public SortExpression OnApplySort(Type resourceType, SortExpression existingSort } /// - public PaginationExpression OnApplyPagination(Type resourceType, PaginationExpression existingPagination) + public PaginationExpression OnApplyPagination(ResourceType resourceType, PaginationExpression existingPagination) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); @@ -65,7 +65,7 @@ public PaginationExpression OnApplyPagination(Type resourceType, PaginationExpre } /// - public SparseFieldSetExpression OnApplySparseFieldSet(Type resourceType, SparseFieldSetExpression existingSparseFieldSet) + public SparseFieldSetExpression OnApplySparseFieldSet(ResourceType resourceType, SparseFieldSetExpression existingSparseFieldSet) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); @@ -74,19 +74,19 @@ public SparseFieldSetExpression OnApplySparseFieldSet(Type resourceType, SparseF } /// - public object GetQueryableHandlerForQueryStringParameter(Type resourceType, string parameterName) + public object GetQueryableHandlerForQueryStringParameter(Type resourceClrType, string parameterName) { - ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(resourceClrType, nameof(resourceClrType)); ArgumentGuard.NotNullNorEmpty(parameterName, nameof(parameterName)); - dynamic resourceDefinition = ResolveResourceDefinition(resourceType); + dynamic resourceDefinition = ResolveResourceDefinition(resourceClrType); dynamic handlers = resourceDefinition.OnRegisterQueryableHandlersForQueryStringParameters(); return handlers != null && handlers.ContainsKey(parameterName) ? handlers[parameterName] : null; } /// - public IDictionary GetMeta(Type resourceType, IIdentifiable resourceInstance) + public IDictionary GetMeta(ResourceType resourceType, IIdentifiable resourceInstance) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); @@ -192,13 +192,17 @@ public void OnSerialize(IIdentifiable resource) resourceDefinition.OnSerialize((dynamic)resource); } - protected virtual object ResolveResourceDefinition(Type resourceType) + protected object ResolveResourceDefinition(Type resourceClrType) { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); + ResourceType resourceType = _resourceGraph.GetResourceType(resourceClrType); + return ResolveResourceDefinition(resourceType); + } - if (resourceContext.IdentityType == typeof(int)) + protected virtual object ResolveResourceDefinition(ResourceType resourceType) + { + if (resourceType.IdentityClrType == typeof(int)) { - Type intResourceDefinitionType = typeof(IResourceDefinition<>).MakeGenericType(resourceContext.ResourceType); + Type intResourceDefinitionType = typeof(IResourceDefinition<>).MakeGenericType(resourceType.ClrType); object intResourceDefinition = _serviceProvider.GetService(intResourceDefinitionType); if (intResourceDefinition != null) @@ -207,7 +211,7 @@ protected virtual object ResolveResourceDefinition(Type resourceType) } } - Type resourceDefinitionType = typeof(IResourceDefinition<,>).MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); + Type resourceDefinitionType = typeof(IResourceDefinition<,>).MakeGenericType(resourceType.ClrType, resourceType.IdentityClrType); return _serviceProvider.GetRequiredService(resourceDefinitionType); } } diff --git a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs index 6ab75ded5b..dd5a03306e 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs @@ -20,11 +20,11 @@ public ResourceFactory(IServiceProvider serviceProvider) } /// - public IIdentifiable CreateInstance(Type resourceType) + public IIdentifiable CreateInstance(Type resourceClrType) { - ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(resourceClrType, nameof(resourceClrType)); - return InnerCreateInstance(resourceType, _serviceProvider); + return InnerCreateInstance(resourceClrType, _serviceProvider); } /// @@ -56,18 +56,18 @@ private static IIdentifiable InnerCreateInstance(Type type, IServiceProvider ser } /// - public NewExpression CreateNewExpression(Type resourceType) + public NewExpression CreateNewExpression(Type resourceClrType) { - ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(resourceClrType, nameof(resourceClrType)); - if (HasSingleConstructorWithoutParameters(resourceType)) + if (HasSingleConstructorWithoutParameters(resourceClrType)) { - return Expression.New(resourceType); + return Expression.New(resourceClrType); } var constructorArguments = new List(); - ConstructorInfo longestConstructor = GetLongestConstructor(resourceType); + ConstructorInfo longestConstructor = GetLongestConstructor(resourceClrType); foreach (ParameterInfo constructorParameter in longestConstructor.GetParameters()) { @@ -84,7 +84,7 @@ public NewExpression CreateNewExpression(Type resourceType) #pragma warning restore AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException { throw new InvalidOperationException( - $"Failed to create an instance of '{resourceType.FullName}': Parameter '{constructorParameter.Name}' could not be resolved.", + $"Failed to create an instance of '{resourceClrType.FullName}': Parameter '{constructorParameter.Name}' could not be resolved.", exception); } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs index 14bf2f3fcb..a5e12175ba 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs @@ -51,7 +51,7 @@ public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConver Type = TryPeekType(ref reader) }; - ResourceContext resourceContext = resourceObject.Type != null ? _resourceGraph.TryGetResourceContext(resourceObject.Type) : null; + ResourceType resourceType = resourceObject.Type != null ? _resourceGraph.TryGetResourceType(resourceObject.Type) : null; while (reader.Read()) { @@ -88,9 +88,9 @@ public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConver } case "attributes": { - if (resourceContext != null) + if (resourceType != null) { - resourceObject.Attributes = ReadAttributes(ref reader, options, resourceContext); + resourceObject.Attributes = ReadAttributes(ref reader, options, resourceType); } else { @@ -159,7 +159,7 @@ private static string TryPeekType(ref Utf8JsonReader reader) return null; } - private static IDictionary ReadAttributes(ref Utf8JsonReader reader, JsonSerializerOptions options, ResourceContext resourceContext) + private static IDictionary ReadAttributes(ref Utf8JsonReader reader, JsonSerializerOptions options, ResourceType resourceType) { var attributes = new Dictionary(); @@ -176,7 +176,7 @@ private static IDictionary ReadAttributes(ref Utf8JsonReader rea string attributeName = reader.GetString(); reader.Read(); - AttrAttribute attribute = resourceContext.TryGetAttributeByPublicName(attributeName); + AttrAttribute attribute = resourceType.TryGetAttributeByPublicName(attributeName); PropertyInfo property = attribute?.Property; if (property != null) diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicOperationObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicOperationObjectAdapter.cs index 54f3b1cceb..317404fafe 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicOperationObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicOperationObjectAdapter.cs @@ -13,19 +13,16 @@ public sealed class AtomicOperationObjectAdapter : IAtomicOperationObjectAdapter private readonly IResourceDataInOperationsRequestAdapter _resourceDataInOperationsRequestAdapter; private readonly IAtomicReferenceAdapter _atomicReferenceAdapter; private readonly IRelationshipDataAdapter _relationshipDataAdapter; - private readonly IResourceGraph _resourceGraph; private readonly IJsonApiOptions _options; - public AtomicOperationObjectAdapter(IResourceGraph resourceGraph, IJsonApiOptions options, IAtomicReferenceAdapter atomicReferenceAdapter, + public AtomicOperationObjectAdapter(IJsonApiOptions options, IAtomicReferenceAdapter atomicReferenceAdapter, IResourceDataInOperationsRequestAdapter resourceDataInOperationsRequestAdapter, IRelationshipDataAdapter relationshipDataAdapter) { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(atomicReferenceAdapter, nameof(atomicReferenceAdapter)); ArgumentGuard.NotNull(resourceDataInOperationsRequestAdapter, nameof(resourceDataInOperationsRequestAdapter)); ArgumentGuard.NotNull(relationshipDataAdapter, nameof(relationshipDataAdapter)); - _resourceGraph = resourceGraph; _options = options; _atomicReferenceAdapter = atomicReferenceAdapter; _resourceDataInOperationsRequestAdapter = resourceDataInOperationsRequestAdapter; @@ -112,7 +109,7 @@ private WriteOperationKind ConvertOperationCode(AtomicOperationObject atomicOper { requirements = new ResourceIdentityRequirements { - ResourceContext = refResult.ResourceContext, + ResourceType = refResult.ResourceType, IdConstraint = requirements.IdConstraint, IdValue = refResult.Resource.StringId, LidValue = refResult.Resource.LocalId, @@ -120,7 +117,7 @@ private WriteOperationKind ConvertOperationCode(AtomicOperationObject atomicOper }; state.WritableRequest.PrimaryId = refResult.Resource.StringId; - state.WritableRequest.PrimaryResource = refResult.ResourceContext; + state.WritableRequest.PrimaryResourceType = refResult.ResourceType; state.WritableRequest.Relationship = refResult.Relationship; state.WritableRequest.IsCollection = refResult.Relationship is HasManyAttribute; @@ -148,8 +145,7 @@ private void ConvertRefRelationship(SingleOrManyData relationshi { if (refResult.Relationship != null) { - ResourceContext resourceContextInData = _resourceGraph.GetResourceContext(refResult.Relationship.RightType); - state.WritableRequest.SecondaryResource = resourceContextInData; + state.WritableRequest.SecondaryResourceType = refResult.Relationship.RightType; state.WritableTargetedFields.Relationships.Add(refResult.Relationship); diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceAdapter.cs index 7c3c5392e6..9ff01bc770 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceAdapter.cs @@ -22,21 +22,21 @@ public AtomicReferenceResult Convert(AtomicReference atomicReference, ResourceId ArgumentGuard.NotNull(state, nameof(state)); using IDisposable _ = state.Position.PushElement("ref"); - (IIdentifiable resource, ResourceContext resourceContext) = ConvertResourceIdentity(atomicReference, requirements, state); + (IIdentifiable resource, ResourceType resourceType) = ConvertResourceIdentity(atomicReference, requirements, state); RelationshipAttribute relationship = atomicReference.Relationship != null - ? ConvertRelationship(atomicReference.Relationship, resourceContext, state) + ? ConvertRelationship(atomicReference.Relationship, resourceType, state) : null; - return new AtomicReferenceResult(resource, resourceContext, relationship); + return new AtomicReferenceResult(resource, resourceType, relationship); } - private RelationshipAttribute ConvertRelationship(string relationshipName, ResourceContext resourceContext, RequestAdapterState state) + private RelationshipAttribute ConvertRelationship(string relationshipName, ResourceType resourceType, RequestAdapterState state) { using IDisposable _ = state.Position.PushElement("relationship"); - RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(relationshipName); + RelationshipAttribute relationship = resourceType.TryGetRelationshipByPublicName(relationshipName); - AssertIsKnownRelationship(relationship, relationshipName, resourceContext, state); + AssertIsKnownRelationship(relationship, relationshipName, resourceType, state); AssertToManyInAddOrRemoveRelationship(relationship, state); return relationship; diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceResult.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceResult.cs index 03879b2b4d..1b85f7021b 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceResult.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicReferenceResult.cs @@ -12,16 +12,16 @@ namespace JsonApiDotNetCore.Serialization.Request.Adapters public sealed class AtomicReferenceResult { public IIdentifiable Resource { get; } - public ResourceContext ResourceContext { get; } + public ResourceType ResourceType { get; } public RelationshipAttribute Relationship { get; } - public AtomicReferenceResult(IIdentifiable resource, ResourceContext resourceContext, RelationshipAttribute relationship) + public AtomicReferenceResult(IIdentifiable resource, ResourceType resourceType, RelationshipAttribute relationship) { ArgumentGuard.NotNull(resource, nameof(resource)); - ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); Resource = resource; - ResourceContext = resourceContext; + ResourceType = resourceType; Relationship = relationship; } } diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentAdapter.cs index 4877f500b7..eea4c7849b 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentAdapter.cs @@ -32,11 +32,11 @@ public object Convert(Document document) { ArgumentGuard.NotNull(document, nameof(document)); - using var context = new RequestAdapterState(_request, _targetedFields); + using var adapterState = new RequestAdapterState(_request, _targetedFields); - return context.Request.Kind == EndpointKind.AtomicOperations - ? _documentInOperationsRequestAdapter.Convert(document, context) - : _documentInResourceOrRelationshipRequestAdapter.Convert(document, context); + return adapterState.Request.Kind == EndpointKind.AtomicOperations + ? _documentInOperationsRequestAdapter.Convert(document, adapterState) + : _documentInResourceOrRelationshipRequestAdapter.Convert(document, adapterState); } } } diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInResourceOrRelationshipRequestAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInResourceOrRelationshipRequestAdapter.cs index 2dd491edce..2a0080cc4b 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInResourceOrRelationshipRequestAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/DocumentInResourceOrRelationshipRequestAdapter.cs @@ -66,7 +66,7 @@ private ResourceIdentityRequirements CreateIdentityRequirements(RequestAdapterSt var requirements = new ResourceIdentityRequirements { - ResourceContext = state.Request.PrimaryResource, + ResourceType = state.Request.PrimaryResourceType, IdConstraint = idConstraint, IdValue = state.Request.PrimaryId }; diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceObjectAdapter.cs index e975e12604..8245444e08 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/IResourceObjectAdapter.cs @@ -13,7 +13,7 @@ public interface IResourceObjectAdapter /// /// Validates and converts the specified . /// - (IIdentifiable resource, ResourceContext resourceContext) Convert(ResourceObject resourceObject, ResourceIdentityRequirements requirements, + (IIdentifiable resource, ResourceType resourceType) Convert(ResourceObject resourceObject, ResourceIdentityRequirements requirements, RequestAdapterState state); } } diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs index be6e571b5f..ad5aebac9e 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; @@ -14,15 +13,12 @@ public sealed class RelationshipDataAdapter : BaseDataAdapter, IRelationshipData { private static readonly CollectionConverter CollectionConverter = new(); - private readonly IResourceGraph _resourceGraph; private readonly IResourceIdentifierObjectAdapter _resourceIdentifierObjectAdapter; - public RelationshipDataAdapter(IResourceGraph resourceGraph, IResourceIdentifierObjectAdapter resourceIdentifierObjectAdapter) + public RelationshipDataAdapter(IResourceIdentifierObjectAdapter resourceIdentifierObjectAdapter) { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(resourceIdentifierObjectAdapter, nameof(resourceIdentifierObjectAdapter)); - _resourceGraph = resourceGraph; _resourceIdentifierObjectAdapter = resourceIdentifierObjectAdapter; } @@ -73,11 +69,10 @@ public object Convert(SingleOrManyData data, Relations AssertHasData(data, state); using IDisposable _ = state.Position.PushElement("data"); - ResourceContext rightResourceContext = _resourceGraph.GetResourceContext(relationship.RightType); var requirements = new ResourceIdentityRequirements { - ResourceContext = rightResourceContext, + ResourceType = relationship.RightType, IdConstraint = JsonElementConstraint.Required, RelationshipName = relationship.PublicName }; diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataAdapter.cs index c1e39a703c..be1fbb1854 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataAdapter.cs @@ -31,7 +31,7 @@ public IIdentifiable Convert(SingleOrManyData data, ResourceIden using IDisposable _ = state.Position.PushElement("data"); AssertHasSingleValue(data, false, state); - (IIdentifiable resource, ResourceContext _) = ConvertResourceObject(data, requirements, state); + (IIdentifiable resource, ResourceType _) = ConvertResourceObject(data, requirements, state); // Ensure that IResourceDefinition extensibility point sees the current operation, it case it injects IJsonApiRequest. state.RefreshInjectables(); @@ -40,7 +40,7 @@ public IIdentifiable Convert(SingleOrManyData data, ResourceIden return resource; } - protected virtual (IIdentifiable resource, ResourceContext resourceContext) ConvertResourceObject(SingleOrManyData data, + protected virtual (IIdentifiable resource, ResourceType resourceType) ConvertResourceObject(SingleOrManyData data, ResourceIdentityRequirements requirements, RequestAdapterState state) { return _resourceObjectAdapter.Convert(data.SingleValue, requirements, state); diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataInOperationsRequestAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataInOperationsRequestAdapter.cs index 68735d84a8..b0e59b6afd 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataInOperationsRequestAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataInOperationsRequestAdapter.cs @@ -12,17 +12,17 @@ public ResourceDataInOperationsRequestAdapter(IResourceDefinitionAccessor resour { } - protected override (IIdentifiable resource, ResourceContext resourceContext) ConvertResourceObject(SingleOrManyData data, + protected override (IIdentifiable resource, ResourceType resourceType) ConvertResourceObject(SingleOrManyData data, ResourceIdentityRequirements requirements, RequestAdapterState state) { // This override ensures that we enrich IJsonApiRequest before calling into IResourceDefinition, so it is ready for consumption there. - (IIdentifiable resource, ResourceContext resourceContext) = base.ConvertResourceObject(data, requirements, state); + (IIdentifiable resource, ResourceType resourceType) = base.ConvertResourceObject(data, requirements, state); - state.WritableRequest.PrimaryResource = resourceContext; + state.WritableRequest.PrimaryResourceType = resourceType; state.WritableRequest.PrimaryId = resource.StringId; - return (resource, resourceContext); + return (resource, resourceType); } } } diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentifierObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentifierObjectAdapter.cs index 029c7c6f8c..fc5cbfc3e4 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentifierObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentifierObjectAdapter.cs @@ -19,7 +19,7 @@ public IIdentifiable Convert(ResourceIdentifierObject resourceIdentifierObject, ArgumentGuard.NotNull(requirements, nameof(requirements)); ArgumentGuard.NotNull(state, nameof(state)); - (IIdentifiable resource, ResourceContext _) = ConvertResourceIdentity(resourceIdentifierObject, requirements, state); + (IIdentifiable resource, _) = ConvertResourceIdentity(resourceIdentifierObject, requirements, state); return resource; } } diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs index 5767711769..618feab797 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs @@ -25,30 +25,30 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory _resourceFactory = resourceFactory; } - protected (IIdentifiable resource, ResourceContext resourceContext) ConvertResourceIdentity(IResourceIdentity identity, + protected (IIdentifiable resource, ResourceType resourceType) ConvertResourceIdentity(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) { ArgumentGuard.NotNull(identity, nameof(identity)); ArgumentGuard.NotNull(requirements, nameof(requirements)); ArgumentGuard.NotNull(state, nameof(state)); - ResourceContext resourceContext = ConvertType(identity, requirements, state); - IIdentifiable resource = CreateResource(identity, requirements, resourceContext.ResourceType, state); + ResourceType resourceType = ConvertType(identity, requirements, state); + IIdentifiable resource = CreateResource(identity, requirements, resourceType.ClrType, state); - return (resource, resourceContext); + return (resource, resourceType); } - private ResourceContext ConvertType(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) + private ResourceType ConvertType(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) { AssertHasType(identity, state); using IDisposable _ = state.Position.PushElement("type"); - ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(identity.Type); + ResourceType resourceType = _resourceGraph.TryGetResourceType(identity.Type); - AssertIsKnownResourceType(resourceContext, identity.Type, state); - AssertIsCompatibleResourceType(resourceContext, requirements.ResourceContext, requirements.RelationshipName, state); + AssertIsKnownResourceType(resourceType, identity.Type, state); + AssertIsCompatibleResourceType(resourceType, requirements.ResourceType, requirements.RelationshipName, state); - return resourceContext; + return resourceType; } private static void AssertHasType(IResourceIdentity identity, RequestAdapterState state) @@ -59,17 +59,17 @@ private static void AssertHasType(IResourceIdentity identity, RequestAdapterStat } } - private static void AssertIsKnownResourceType(ResourceContext resourceContext, string typeName, RequestAdapterState state) + private static void AssertIsKnownResourceType(ResourceType resourceType, string typeName, RequestAdapterState state) { - if (resourceContext == null) + if (resourceType == null) { throw new ModelConversionException(state.Position, "Unknown resource type found.", $"Resource type '{typeName}' does not exist."); } } - private static void AssertIsCompatibleResourceType(ResourceContext actual, ResourceContext expected, string relationshipName, RequestAdapterState state) + private static void AssertIsCompatibleResourceType(ResourceType actual, ResourceType expected, string relationshipName, RequestAdapterState state) { - if (expected != null && !expected.ResourceType.IsAssignableFrom(actual.ResourceType)) + if (expected != null && !expected.ClrType.IsAssignableFrom(actual.ClrType)) { string message = relationshipName != null ? $"Type '{actual.PublicName}' is incompatible with type '{expected.PublicName}' of relationship '{relationshipName}'." @@ -79,7 +79,7 @@ private static void AssertIsCompatibleResourceType(ResourceContext actual, Resou } } - private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentityRequirements requirements, Type resourceType, + private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentityRequirements requirements, Type resourceClrType, RequestAdapterState state) { if (state.Request.Kind != EndpointKind.AtomicOperations) @@ -101,7 +101,7 @@ private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentit AssertSameIdValue(identity, requirements.IdValue, state); AssertSameLidValue(identity, requirements.LidValue, state); - IIdentifiable resource = _resourceFactory.CreateInstance(resourceType); + IIdentifiable resource = _resourceFactory.CreateInstance(resourceClrType); AssignStringId(identity, resource, state); resource.LocalId = identity.Lid; return resource; @@ -194,13 +194,13 @@ private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, } } - protected static void AssertIsKnownRelationship(RelationshipAttribute relationship, string relationshipName, ResourceContext resourceContext, + protected static void AssertIsKnownRelationship(RelationshipAttribute relationship, string relationshipName, ResourceType resourceType, RequestAdapterState state) { if (relationship == null) { throw new ModelConversionException(state.Position, "Unknown relationship found.", - $"Relationship '{relationshipName}' does not exist on resource type '{resourceContext.PublicName}'."); + $"Relationship '{relationshipName}' does not exist on resource type '{resourceType.PublicName}'."); } } diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs index a9386be562..601212ec93 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs @@ -13,7 +13,7 @@ public sealed class ResourceIdentityRequirements /// /// When not null, indicates that the "type" element must be compatible with the specified resource type. /// - public ResourceContext ResourceContext { get; init; } + public ResourceType ResourceType { get; init; } /// /// When not null, indicates the presence or absence of the "id" element. diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs index 5ac7021641..5c49a4e0e4 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs @@ -27,60 +27,59 @@ public ResourceObjectAdapter(IResourceGraph resourceGraph, IResourceFactory reso } /// - public (IIdentifiable resource, ResourceContext resourceContext) Convert(ResourceObject resourceObject, ResourceIdentityRequirements requirements, + public (IIdentifiable resource, ResourceType resourceType) Convert(ResourceObject resourceObject, ResourceIdentityRequirements requirements, RequestAdapterState state) { ArgumentGuard.NotNull(resourceObject, nameof(resourceObject)); ArgumentGuard.NotNull(requirements, nameof(requirements)); ArgumentGuard.NotNull(state, nameof(state)); - (IIdentifiable resource, ResourceContext resourceContext) = ConvertResourceIdentity(resourceObject, requirements, state); + (IIdentifiable resource, ResourceType resourceType) = ConvertResourceIdentity(resourceObject, requirements, state); - ConvertAttributes(resourceObject.Attributes, resource, resourceContext, state); - ConvertRelationships(resourceObject.Relationships, resource, resourceContext, state); + ConvertAttributes(resourceObject.Attributes, resource, resourceType, state); + ConvertRelationships(resourceObject.Relationships, resource, resourceType, state); - return (resource, resourceContext); + return (resource, resourceType); } - private void ConvertAttributes(IDictionary resourceObjectAttributes, IIdentifiable resource, ResourceContext resourceContext, + private void ConvertAttributes(IDictionary resourceObjectAttributes, IIdentifiable resource, ResourceType resourceType, RequestAdapterState state) { using IDisposable _ = state.Position.PushElement("attributes"); foreach ((string attributeName, object attributeValue) in resourceObjectAttributes.EmptyIfNull()) { - ConvertAttribute(resource, attributeName, attributeValue, resourceContext, state); + ConvertAttribute(resource, attributeName, attributeValue, resourceType, state); } } - private void ConvertAttribute(IIdentifiable resource, string attributeName, object attributeValue, ResourceContext resourceContext, - RequestAdapterState state) + private void ConvertAttribute(IIdentifiable resource, string attributeName, object attributeValue, ResourceType resourceType, RequestAdapterState state) { using IDisposable _ = state.Position.PushElement(attributeName); - AttrAttribute attr = resourceContext.TryGetAttributeByPublicName(attributeName); + AttrAttribute attr = resourceType.TryGetAttributeByPublicName(attributeName); if (attr == null && _options.AllowUnknownFieldsInRequestBody) { return; } - AssertIsKnownAttribute(attr, attributeName, resourceContext, state); + AssertIsKnownAttribute(attr, attributeName, resourceType, state); AssertNoInvalidAttribute(attributeValue, state); - AssertNoBlockedCreate(attr, resourceContext, state); - AssertNoBlockedChange(attr, resourceContext, state); - AssertNotReadOnly(attr, resourceContext, state); + AssertNoBlockedCreate(attr, resourceType, state); + AssertNoBlockedChange(attr, resourceType, state); + AssertNotReadOnly(attr, resourceType, state); attr!.SetValue(resource, attributeValue); state.WritableTargetedFields.Attributes.Add(attr); } [AssertionMethod] - private static void AssertIsKnownAttribute(AttrAttribute attr, string attributeName, ResourceContext resourceContext, RequestAdapterState state) + private static void AssertIsKnownAttribute(AttrAttribute attr, string attributeName, ResourceType resourceType, RequestAdapterState state) { if (attr == null) { throw new ModelConversionException(state.Position, "Unknown attribute found.", - $"Attribute '{attributeName}' does not exist on resource type '{resourceContext.PublicName}'."); + $"Attribute '{attributeName}' does not exist on resource type '{resourceType.PublicName}'."); } } @@ -100,56 +99,56 @@ private static void AssertNoInvalidAttribute(object attributeValue, RequestAdapt } } - private static void AssertNoBlockedCreate(AttrAttribute attr, ResourceContext resourceContext, RequestAdapterState state) + private static void AssertNoBlockedCreate(AttrAttribute attr, ResourceType resourceType, RequestAdapterState state) { if (state.Request.WriteOperation == WriteOperationKind.CreateResource && !attr.Capabilities.HasFlag(AttrCapabilities.AllowCreate)) { throw new ModelConversionException(state.Position, "Attribute value cannot be assigned when creating resource.", - $"The attribute '{attr.PublicName}' on resource type '{resourceContext.PublicName}' cannot be assigned to."); + $"The attribute '{attr.PublicName}' on resource type '{resourceType.PublicName}' cannot be assigned to."); } } - private static void AssertNoBlockedChange(AttrAttribute attr, ResourceContext resourceContext, RequestAdapterState state) + private static void AssertNoBlockedChange(AttrAttribute attr, ResourceType resourceType, RequestAdapterState state) { if (state.Request.WriteOperation == WriteOperationKind.UpdateResource && !attr.Capabilities.HasFlag(AttrCapabilities.AllowChange)) { throw new ModelConversionException(state.Position, "Attribute value cannot be assigned when updating resource.", - $"The attribute '{attr.PublicName}' on resource type '{resourceContext.PublicName}' cannot be assigned to."); + $"The attribute '{attr.PublicName}' on resource type '{resourceType.PublicName}' cannot be assigned to."); } } - private static void AssertNotReadOnly(AttrAttribute attr, ResourceContext resourceContext, RequestAdapterState state) + private static void AssertNotReadOnly(AttrAttribute attr, ResourceType resourceType, RequestAdapterState state) { if (attr.Property.SetMethod == null) { throw new ModelConversionException(state.Position, "Attribute is read-only.", - $"Attribute '{attr.PublicName}' on resource type '{resourceContext.PublicName}' is read-only."); + $"Attribute '{attr.PublicName}' on resource type '{resourceType.PublicName}' is read-only."); } } private void ConvertRelationships(IDictionary resourceObjectRelationships, IIdentifiable resource, - ResourceContext resourceContext, RequestAdapterState state) + ResourceType resourceType, RequestAdapterState state) { using IDisposable _ = state.Position.PushElement("relationships"); foreach ((string relationshipName, RelationshipObject relationshipObject) in resourceObjectRelationships.EmptyIfNull()) { - ConvertRelationship(relationshipName, relationshipObject.Data, resource, resourceContext, state); + ConvertRelationship(relationshipName, relationshipObject.Data, resource, resourceType, state); } } private void ConvertRelationship(string relationshipName, SingleOrManyData relationshipData, IIdentifiable resource, - ResourceContext resourceContext, RequestAdapterState state) + ResourceType resourceType, RequestAdapterState state) { using IDisposable _ = state.Position.PushElement(relationshipName); - RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(relationshipName); + RelationshipAttribute relationship = resourceType.TryGetRelationshipByPublicName(relationshipName); if (relationship == null && _options.AllowUnknownFieldsInRequestBody) { return; } - AssertIsKnownRelationship(relationship, relationshipName, resourceContext, state); + AssertIsKnownRelationship(relationship, relationshipName, resourceType, state); object rightValue = _relationshipDataAdapter.Convert(relationshipData, relationship, true, state); diff --git a/src/JsonApiDotNetCore/Serialization/Response/ILinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/ILinkBuilder.cs index f49859f132..ce3e027410 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/ILinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/ILinkBuilder.cs @@ -1,3 +1,4 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; @@ -17,7 +18,7 @@ public interface ILinkBuilder /// /// Builds the links object for a returned resource (primary or included). /// - ResourceLinks GetResourceLinks(string resourceName, string id); + ResourceLinks GetResourceLinks(ResourceType resourceType, string id); /// /// Builds the links object for a relationship inside a returned resource. diff --git a/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs index 4b0231d186..4871bd7fdb 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs @@ -31,25 +31,22 @@ public class LinkBuilder : ILinkBuilder private readonly IJsonApiOptions _options; private readonly IJsonApiRequest _request; private readonly IPaginationContext _paginationContext; - private readonly IResourceGraph _resourceGraph; private readonly IHttpContextAccessor _httpContextAccessor; private readonly LinkGenerator _linkGenerator; private readonly IControllerResourceMapping _controllerResourceMapping; - public LinkBuilder(IJsonApiOptions options, IJsonApiRequest request, IPaginationContext paginationContext, IResourceGraph resourceGraph, - IHttpContextAccessor httpContextAccessor, LinkGenerator linkGenerator, IControllerResourceMapping controllerResourceMapping) + public LinkBuilder(IJsonApiOptions options, IJsonApiRequest request, IPaginationContext paginationContext, IHttpContextAccessor httpContextAccessor, + LinkGenerator linkGenerator, IControllerResourceMapping controllerResourceMapping) { ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(paginationContext, nameof(paginationContext)); - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(linkGenerator, nameof(linkGenerator)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); _options = options; _request = request; _paginationContext = paginationContext; - _resourceGraph = resourceGraph; _httpContextAccessor = httpContextAccessor; _linkGenerator = linkGenerator; _controllerResourceMapping = controllerResourceMapping; @@ -64,36 +61,35 @@ private static string NoAsyncSuffix(string actionName) public TopLevelLinks GetTopLevelLinks() { var links = new TopLevelLinks(); + ResourceType resourceType = _request.SecondaryResourceType ?? _request.PrimaryResourceType; - ResourceContext requestContext = _request.SecondaryResource ?? _request.PrimaryResource; - - if (ShouldIncludeTopLevelLink(LinkTypes.Self, requestContext)) + if (ShouldIncludeTopLevelLink(LinkTypes.Self, resourceType)) { links.Self = GetLinkForTopLevelSelf(); } - if (_request.Kind == EndpointKind.Relationship && _request.Relationship != null && ShouldIncludeTopLevelLink(LinkTypes.Related, requestContext)) + if (_request.Kind == EndpointKind.Relationship && _request.Relationship != null && ShouldIncludeTopLevelLink(LinkTypes.Related, resourceType)) { links.Related = GetLinkForRelationshipRelated(_request.PrimaryId, _request.Relationship); } - if (_request.IsCollection && _paginationContext.PageSize != null && ShouldIncludeTopLevelLink(LinkTypes.Paging, requestContext)) + if (_request.IsCollection && _paginationContext.PageSize != null && ShouldIncludeTopLevelLink(LinkTypes.Paging, resourceType)) { - SetPaginationInTopLevelLinks(requestContext, links); + SetPaginationInTopLevelLinks(resourceType, links); } return links.HasValue() ? links : null; } /// - /// Checks if the top-level should be added by first checking configuration on the , and if - /// not configured, by checking with the global configuration in . + /// Checks if the top-level should be added by first checking configuration on the , and if not + /// configured, by checking with the global configuration in . /// - private bool ShouldIncludeTopLevelLink(LinkTypes linkType, ResourceContext resourceContext) + private bool ShouldIncludeTopLevelLink(LinkTypes linkType, ResourceType resourceType) { - if (resourceContext != null && resourceContext.TopLevelLinks != LinkTypes.NotConfigured) + if (resourceType != null && resourceType.TopLevelLinks != LinkTypes.NotConfigured) { - return resourceContext.TopLevelLinks.HasFlag(linkType); + return resourceType.TopLevelLinks.HasFlag(linkType); } return _options.TopLevelLinks.HasFlag(linkType); @@ -106,9 +102,9 @@ private string GetLinkForTopLevelSelf() : _httpContextAccessor.HttpContext!.Request.GetEncodedUrl(); } - private void SetPaginationInTopLevelLinks(ResourceContext requestContext, TopLevelLinks links) + private void SetPaginationInTopLevelLinks(ResourceType resourceType, TopLevelLinks links) { - string pageSizeValue = CalculatePageSizeValue(_paginationContext.PageSize, requestContext); + string pageSizeValue = CalculatePageSizeValue(_paginationContext.PageSize, resourceType); links.First = GetLinkForPagination(1, pageSizeValue); @@ -131,17 +127,17 @@ private void SetPaginationInTopLevelLinks(ResourceContext requestContext, TopLev } } - private string CalculatePageSizeValue(PageSize topPageSize, ResourceContext requestContext) + private string CalculatePageSizeValue(PageSize topPageSize, ResourceType resourceType) { string pageSizeParameterValue = _httpContextAccessor.HttpContext!.Request.Query[PageSizeParameterName]; PageSize newTopPageSize = Equals(topPageSize, _options.DefaultPageSize) ? null : topPageSize; - return ChangeTopPageSize(pageSizeParameterValue, newTopPageSize, requestContext); + return ChangeTopPageSize(pageSizeParameterValue, newTopPageSize, resourceType); } - private string ChangeTopPageSize(string pageSizeParameterValue, PageSize topPageSize, ResourceContext requestContext) + private string ChangeTopPageSize(string pageSizeParameterValue, PageSize topPageSize, ResourceType resourceType) { - IImmutableList elements = ParsePageSizeExpression(pageSizeParameterValue, requestContext); + IImmutableList elements = ParsePageSizeExpression(pageSizeParameterValue, resourceType); int elementInTopScopeIndex = elements.FindIndex(expression => expression.Scope == null); if (topPageSize != null) @@ -164,16 +160,15 @@ private string ChangeTopPageSize(string pageSizeParameterValue, PageSize topPage return parameterValue == string.Empty ? null : parameterValue; } - private IImmutableList ParsePageSizeExpression(string pageSizeParameterValue, - ResourceContext requestResource) + private IImmutableList ParsePageSizeExpression(string pageSizeParameterValue, ResourceType resourceType) { if (pageSizeParameterValue == null) { return ImmutableArray.Empty; } - var parser = new PaginationParser(_resourceGraph); - PaginationQueryStringValueExpression paginationExpression = parser.Parse(pageSizeParameterValue, requestResource); + var parser = new PaginationParser(); + PaginationQueryStringValueExpression paginationExpression = parser.Parse(pageSizeParameterValue, resourceType); return paginationExpression.Elements; } @@ -224,39 +219,38 @@ private static string DecodeSpecialCharacters(string uri) } /// - public ResourceLinks GetResourceLinks(string resourceName, string id) + public ResourceLinks GetResourceLinks(ResourceType resourceType, string id) { - ArgumentGuard.NotNullNorEmpty(resourceName, nameof(resourceName)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); ArgumentGuard.NotNullNorEmpty(id, nameof(id)); var links = new ResourceLinks(); - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceName); - if (ShouldIncludeResourceLink(LinkTypes.Self, resourceContext)) + if (ShouldIncludeResourceLink(LinkTypes.Self, resourceType)) { - links.Self = GetLinkForResourceSelf(resourceContext, id); + links.Self = GetLinkForResourceSelf(resourceType, id); } return links.HasValue() ? links : null; } /// - /// Checks if the resource object level should be added by first checking configuration on the - /// , and if not configured, by checking with the global configuration in . + /// Checks if the resource object level should be added by first checking configuration on the , + /// and if not configured, by checking with the global configuration in . /// - private bool ShouldIncludeResourceLink(LinkTypes linkType, ResourceContext resourceContext) + private bool ShouldIncludeResourceLink(LinkTypes linkType, ResourceType resourceType) { - if (resourceContext.ResourceLinks != LinkTypes.NotConfigured) + if (resourceType.ResourceLinks != LinkTypes.NotConfigured) { - return resourceContext.ResourceLinks.HasFlag(linkType); + return resourceType.ResourceLinks.HasFlag(linkType); } return _options.ResourceLinks.HasFlag(linkType); } - private string GetLinkForResourceSelf(ResourceContext resourceContext, string resourceId) + private string GetLinkForResourceSelf(ResourceType resourceType, string resourceId) { - string controllerName = _controllerResourceMapping.GetControllerNameForResourceType(resourceContext.ResourceType); + string controllerName = _controllerResourceMapping.TryGetControllerNameForResourceType(resourceType); IDictionary routeValues = GetRouteValues(resourceId, null); return RenderLinkForAction(controllerName, GetPrimaryControllerActionName, routeValues); @@ -269,14 +263,13 @@ public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship ArgumentGuard.NotNull(leftResource, nameof(leftResource)); var links = new RelationshipLinks(); - ResourceContext leftResourceContext = _resourceGraph.GetResourceContext(leftResource.GetType()); - if (ShouldIncludeRelationshipLink(LinkTypes.Self, relationship, leftResourceContext)) + if (ShouldIncludeRelationshipLink(LinkTypes.Self, relationship)) { links.Self = GetLinkForRelationshipSelf(leftResource.StringId, relationship); } - if (ShouldIncludeRelationshipLink(LinkTypes.Related, relationship, leftResourceContext)) + if (ShouldIncludeRelationshipLink(LinkTypes.Related, relationship)) { links.Related = GetLinkForRelationshipRelated(leftResource.StringId, relationship); } @@ -286,7 +279,7 @@ public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship private string GetLinkForRelationshipSelf(string leftId, RelationshipAttribute relationship) { - string controllerName = _controllerResourceMapping.GetControllerNameForResourceType(relationship.LeftType); + string controllerName = _controllerResourceMapping.TryGetControllerNameForResourceType(relationship.LeftType); IDictionary routeValues = GetRouteValues(leftId, relationship.PublicName); return RenderLinkForAction(controllerName, GetRelationshipControllerActionName, routeValues); @@ -294,7 +287,7 @@ private string GetLinkForRelationshipSelf(string leftId, RelationshipAttribute r private string GetLinkForRelationshipRelated(string leftId, RelationshipAttribute relationship) { - string controllerName = _controllerResourceMapping.GetControllerNameForResourceType(relationship.LeftType); + string controllerName = _controllerResourceMapping.TryGetControllerNameForResourceType(relationship.LeftType); IDictionary routeValues = GetRouteValues(leftId, relationship.PublicName); return RenderLinkForAction(controllerName, GetSecondaryControllerActionName, routeValues); @@ -325,16 +318,16 @@ protected virtual string RenderLinkForAction(string controllerName, string actio /// attribute, if not configured by checking on the resource /// type that contains this relationship, and if not configured by checking with the global configuration in . /// - private bool ShouldIncludeRelationshipLink(LinkTypes linkType, RelationshipAttribute relationship, ResourceContext leftResourceContext) + private bool ShouldIncludeRelationshipLink(LinkTypes linkType, RelationshipAttribute relationship) { if (relationship.Links != LinkTypes.NotConfigured) { return relationship.Links.HasFlag(linkType); } - if (leftResourceContext.RelationshipLinks != LinkTypes.NotConfigured) + if (relationship.LeftType.RelationshipLinks != LinkTypes.NotConfigured) { - return leftResourceContext.RelationshipLinks.HasFlag(linkType); + return relationship.LeftType.RelationshipLinks.HasFlag(linkType); } return _options.RelationshipLinks.HasFlag(linkType); diff --git a/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs index 8cce46d53e..a8a0c7cff7 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs @@ -23,7 +23,6 @@ public sealed class ResponseModelAdapter : IResponseModelAdapter private readonly IJsonApiRequest _request; private readonly IJsonApiOptions _options; - private readonly IResourceGraph _resourceGraph; private readonly ILinkBuilder _linkBuilder; private readonly IMetaBuilder _metaBuilder; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; @@ -31,13 +30,12 @@ public sealed class ResponseModelAdapter : IResponseModelAdapter private readonly IRequestQueryStringAccessor _requestQueryStringAccessor; private readonly ISparseFieldSetCache _sparseFieldSetCache; - public ResponseModelAdapter(IJsonApiRequest request, IJsonApiOptions options, IResourceGraph resourceGraph, ILinkBuilder linkBuilder, - IMetaBuilder metaBuilder, IResourceDefinitionAccessor resourceDefinitionAccessor, IEvaluatedIncludeCache evaluatedIncludeCache, - ISparseFieldSetCache sparseFieldSetCache, IRequestQueryStringAccessor requestQueryStringAccessor) + public ResponseModelAdapter(IJsonApiRequest request, IJsonApiOptions options, ILinkBuilder linkBuilder, IMetaBuilder metaBuilder, + IResourceDefinitionAccessor resourceDefinitionAccessor, IEvaluatedIncludeCache evaluatedIncludeCache, ISparseFieldSetCache sparseFieldSetCache, + IRequestQueryStringAccessor requestQueryStringAccessor) { ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(options, nameof(options)); - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder)); ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); @@ -47,7 +45,6 @@ public ResponseModelAdapter(IJsonApiRequest request, IJsonApiOptions options, IR _request = request; _options = options; - _resourceGraph = resourceGraph; _linkBuilder = linkBuilder; _metaBuilder = metaBuilder; _resourceDefinitionAccessor = resourceDefinitionAccessor; @@ -67,17 +64,18 @@ public Document Convert(object model) IImmutableSet includeElements = include?.Elements ?? ImmutableHashSet.Empty; var includedCollection = new IncludedCollection(); + ResourceType resourceType = _request.SecondaryResourceType ?? _request.PrimaryResourceType; if (model is IEnumerable resources) { - IEnumerable resourceObjects = - resources.Select(resource => ConvertResource(resource, _request.Kind, includeElements, includedCollection, false)); + IEnumerable resourceObjects = resources.Select(resource => + ConvertResource(resource, resourceType, _request.Kind, includeElements, includedCollection, false)); document.Data = new SingleOrManyData(resourceObjects); } else if (model is IIdentifiable resource) { - ResourceObject resourceObject = ConvertResource(resource, _request.Kind, includeElements, includedCollection, false); + ResourceObject resourceObject = ConvertResource(resource, resourceType, _request.Kind, includeElements, includedCollection, false); document.Data = new SingleOrManyData(resourceObject); } else if (model == null) @@ -119,7 +117,8 @@ private AtomicResultObject ConvertOperation(OperationContainer operation, IImmut { _request.CopyFrom(operation.Request); - resourceObject = ConvertResource(operation.Resource, operation.Request.Kind, includeElements, includedCollection, false); + ResourceType resourceType = operation.Request.SecondaryResourceType ?? operation.Request.PrimaryResourceType; + resourceObject = ConvertResource(operation.Resource, resourceType, operation.Request.Kind, includeElements, includedCollection, false); _sparseFieldSetCache.Reset(); } @@ -130,17 +129,16 @@ private AtomicResultObject ConvertOperation(OperationContainer operation, IImmut }; } - private ResourceObject ConvertResource(IIdentifiable resource, EndpointKind requestKind, IImmutableSet includeElements, - IncludedCollection includedCollection, bool isInclude) + private ResourceObject ConvertResource(IIdentifiable resource, ResourceType resourceType, EndpointKind requestKind, + IImmutableSet includeElements, IncludedCollection includedCollection, bool isInclude) { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(resource.GetType()); IImmutableSet fieldSet = null; if (requestKind != EndpointKind.Relationship) { _resourceDefinitionAccessor.OnSerialize(resource); - fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); + fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceType); } var resourceObject = new ResourceObject(); @@ -152,24 +150,23 @@ private ResourceObject ConvertResource(IIdentifiable resource, EndpointKind requ bool isRelationship = requestKind == EndpointKind.Relationship; - resourceObject.Type = resourceContext.PublicName; + resourceObject.Type = resourceType.PublicName; resourceObject.Id = resource.StringId; - resourceObject.Attributes = ConvertAttributes(resource, resourceContext, fieldSet); - resourceObject.Relationships = ConvertRelationships(resource, resourceContext, fieldSet, requestKind, includeElements, includedCollection); - resourceObject.Links = isRelationship ? null : _linkBuilder.GetResourceLinks(resourceContext.PublicName, resource.StringId); - resourceObject.Meta = isRelationship ? null : _resourceDefinitionAccessor.GetMeta(resource.GetType(), resource); + resourceObject.Attributes = ConvertAttributes(resource, resourceType, fieldSet); + resourceObject.Relationships = ConvertRelationships(resource, resourceType, fieldSet, requestKind, includeElements, includedCollection); + resourceObject.Links = isRelationship ? null : _linkBuilder.GetResourceLinks(resourceType, resource.StringId); + resourceObject.Meta = isRelationship ? null : _resourceDefinitionAccessor.GetMeta(resourceType, resource); return resourceObject; } - private IDictionary ConvertAttributes(IIdentifiable resource, ResourceContext resourceContext, - IImmutableSet fieldSet) + private IDictionary ConvertAttributes(IIdentifiable resource, ResourceType resourceType, IImmutableSet fieldSet) { if (fieldSet != null) { - var attrMap = new Dictionary(resourceContext.Attributes.Count); + var attrMap = new Dictionary(resourceType.Attributes.Count); - foreach (AttrAttribute attr in resourceContext.Attributes) + foreach (AttrAttribute attr in resourceType.Attributes) { if (!fieldSet.Contains(attr) || attr.Property.Name == nameof(Identifiable.Id)) { @@ -201,15 +198,15 @@ private IDictionary ConvertAttributes(IIdentifiable resource, Re return null; } - private IDictionary ConvertRelationships(IIdentifiable resource, ResourceContext resourceContext, + private IDictionary ConvertRelationships(IIdentifiable resource, ResourceType resourceType, IImmutableSet fieldSet, EndpointKind requestKind, IImmutableSet includeElements, IncludedCollection includedCollection) { if (fieldSet != null) { - var relationshipMap = new Dictionary(resourceContext.Relationships.Count); + var relationshipMap = new Dictionary(resourceType.Relationships.Count); - foreach (RelationshipAttribute relationship in resourceContext.Relationships) + foreach (RelationshipAttribute relationship in resourceType.Relationships) { IncludeElementExpression includeElement = GetFirstOrDefault(includeElements, relationship, (element, nextRelationship) => element.Relationship.Equals(nextRelationship)); @@ -263,13 +260,15 @@ private RelationshipObject ConvertRelationship(RelationshipAttribute relationshi { var resourceIdentifierObject = new ResourceIdentifierObject { - Type = _resourceGraph.GetResourceContext(rightResource.GetType()).PublicName, + Type = relationship.RightType.PublicName, Id = rightResource.StringId }; resourceIdentifierObjects.Add(resourceIdentifierObject); - ResourceObject includeResource = ConvertResource(rightResource, requestKind, includeElement.Children, includedCollection, true); + ResourceObject includeResource = ConvertResource(rightResource, relationship.RightType, requestKind, includeElement.Children, + includedCollection, true); + includedCollection.AddOrUpdate(rightResource, includeResource); } diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index 9b659a4cdf..feb508cf56 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -66,7 +66,7 @@ public virtual async Task> GetAsync(CancellationT if (_options.IncludeTotalResourceCount) { - FilterExpression topFilter = _queryLayerComposer.GetTopFilterFromConstraints(_request.PrimaryResource); + FilterExpression topFilter = _queryLayerComposer.GetTopFilterFromConstraints(_request.PrimaryResourceType); _paginationContext.TotalResourceCount = await _repositoryAccessor.CountAsync(topFilter, cancellationToken); if (_paginationContext.TotalResourceCount == 0) @@ -75,7 +75,7 @@ public virtual async Task> GetAsync(CancellationT } } - QueryLayer queryLayer = _queryLayerComposer.ComposeFromConstraints(_request.PrimaryResource); + QueryLayer queryLayer = _queryLayerComposer.ComposeFromConstraints(_request.PrimaryResourceType); IReadOnlyCollection resources = await _repositoryAccessor.GetAsync(queryLayer, cancellationToken); if (queryLayer.Pagination?.PageSize != null && queryLayer.Pagination.PageSize.Value == resources.Count) @@ -112,8 +112,10 @@ public virtual async Task GetSecondaryAsync(TId id, string relationshipN AssertHasRelationship(_request.Relationship, relationshipName); - QueryLayer secondaryLayer = _queryLayerComposer.ComposeFromConstraints(_request.SecondaryResource); - QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship); + QueryLayer secondaryLayer = _queryLayerComposer.ComposeFromConstraints(_request.SecondaryResourceType); + + QueryLayer primaryLayer = + _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResourceType, id, _request.Relationship); IReadOnlyCollection primaryResources = await _repositoryAccessor.GetAsync(primaryLayer, cancellationToken); @@ -145,8 +147,10 @@ public virtual async Task GetRelationshipAsync(TId id, string relationsh AssertHasRelationship(_request.Relationship, relationshipName); - QueryLayer secondaryLayer = _queryLayerComposer.ComposeSecondaryLayerForRelationship(_request.SecondaryResource); - QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship); + QueryLayer secondaryLayer = _queryLayerComposer.ComposeSecondaryLayerForRelationship(_request.SecondaryResourceType); + + QueryLayer primaryLayer = + _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResourceType, id, _request.Relationship); IReadOnlyCollection primaryResources = await _repositoryAccessor.GetAsync(primaryLayer, cancellationToken); @@ -190,7 +194,7 @@ public virtual async Task CreateAsync(TResource resource, Cancellatio if (existingResource != null) { - throw new ResourceAlreadyExistsException(resourceFromRequest.StringId, _request.PrimaryResource.PublicName); + throw new ResourceAlreadyExistsException(resourceFromRequest.StringId, _request.PrimaryResourceType.PublicName); } } @@ -236,8 +240,8 @@ protected async Task AssertResourcesToAssignInRelationshipsExistAsync(TResource private async IAsyncEnumerable GetMissingRightResourcesAsync(QueryLayer existingRightResourceIdsQueryLayer, RelationshipAttribute relationship, ICollection rightResourceIds, [EnumeratorCancellation] CancellationToken cancellationToken) { - IReadOnlyCollection existingResources = await _repositoryAccessor.GetAsync( - existingRightResourceIdsQueryLayer.ResourceContext.ResourceType, existingRightResourceIdsQueryLayer, cancellationToken); + IReadOnlyCollection existingResources = await _repositoryAccessor.GetAsync(existingRightResourceIdsQueryLayer.ResourceType, + existingRightResourceIdsQueryLayer, cancellationToken); string[] existingResourceIds = existingResources.Select(resource => resource.StringId).ToArray(); @@ -245,7 +249,7 @@ private async IAsyncEnumerable GetMissingRightRes { if (!existingResourceIds.Contains(rightResourceId.StringId)) { - yield return new MissingResourceInRelationship(relationship.PublicName, existingRightResourceIdsQueryLayer.ResourceContext.PublicName, + yield return new MissingResourceInRelationship(relationship.PublicName, existingRightResourceIdsQueryLayer.ResourceType.PublicName, rightResourceId.StringId); } } @@ -456,7 +460,7 @@ protected async Task GetPrimaryResourceByIdAsync(TId id, TopFieldSele private async Task TryGetPrimaryResourceByIdAsync(TId id, TopFieldSelection fieldSelection, CancellationToken cancellationToken) { - QueryLayer primaryLayer = _queryLayerComposer.ComposeForGetById(id, _request.PrimaryResource, fieldSelection); + QueryLayer primaryLayer = _queryLayerComposer.ComposeForGetById(id, _request.PrimaryResourceType, fieldSelection); IReadOnlyCollection primaryResources = await _repositoryAccessor.GetAsync(primaryLayer, cancellationToken); return primaryResources.SingleOrDefault(); @@ -464,7 +468,7 @@ private async Task TryGetPrimaryResourceByIdAsync(TId id, TopFieldSel protected async Task GetPrimaryResourceForUpdateAsync(TId id, CancellationToken cancellationToken) { - QueryLayer queryLayer = _queryLayerComposer.ComposeForUpdate(id, _request.PrimaryResource); + QueryLayer queryLayer = _queryLayerComposer.ComposeForUpdate(id, _request.PrimaryResourceType); var resource = await _repositoryAccessor.GetForUpdateAsync(queryLayer, cancellationToken); AssertPrimaryResourceExists(resource); @@ -476,7 +480,7 @@ private void AssertPrimaryResourceExists(TResource resource) { if (resource == null) { - throw new ResourceNotFoundException(_request.PrimaryId, _request.PrimaryResource.PublicName); + throw new ResourceNotFoundException(_request.PrimaryId, _request.PrimaryResourceType.PublicName); } } @@ -485,7 +489,7 @@ private void AssertHasRelationship(RelationshipAttribute relationship, string na { if (relationship == null) { - throw new RelationshipNotFoundException(name, _request.PrimaryResource.PublicName); + throw new RelationshipNotFoundException(name, _request.PrimaryResourceType.PublicName); } } } diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index a5a2f9fd77..1599ae57c2 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -56,11 +56,11 @@ public void Can_add_resources_from_assembly_to_graph() // Assert IResourceGraph resourceGraph = _resourceGraphBuilder.Build(); - ResourceContext personContext = resourceGraph.TryGetResourceContext(typeof(Person)); - personContext.Should().NotBeNull(); + ResourceType personType = resourceGraph.TryGetResourceType(typeof(Person)); + personType.Should().NotBeNull(); - ResourceContext todoItemContext = resourceGraph.TryGetResourceContext(typeof(TodoItem)); - todoItemContext.Should().NotBeNull(); + ResourceType todoItemType = resourceGraph.TryGetResourceType(typeof(TodoItem)); + todoItemType.Should().NotBeNull(); } [Fact] @@ -76,8 +76,8 @@ public void Can_add_resource_from_current_assembly_to_graph() // Assert IResourceGraph resourceGraph = _resourceGraphBuilder.Build(); - ResourceContext testContext = resourceGraph.TryGetResourceContext(typeof(TestResource)); - testContext.Should().NotBeNull(); + ResourceType testResourceType = resourceGraph.TryGetResourceType(typeof(TestResource)); + testResourceType.Should().NotBeNull(); } [Fact] diff --git a/test/DiscoveryTests/TestResourceRepository.cs b/test/DiscoveryTests/TestResourceRepository.cs index 096da8abd9..a42d1603f3 100644 --- a/test/DiscoveryTests/TestResourceRepository.cs +++ b/test/DiscoveryTests/TestResourceRepository.cs @@ -11,10 +11,10 @@ namespace DiscoveryTests [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class TestResourceRepository : EntityFrameworkCoreRepository { - public TestResourceRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + public TestResourceRepository(ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) + : base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs index 862612d978..f6b7148e3e 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.GetAttributeByPropertyName(nameof(TelevisionBroadcast.ArchivedAt)); + AttrAttribute archivedAtAttribute = ResourceType.GetAttributeByPropertyName(nameof(TelevisionBroadcast.ArchivedAt)); var archivedAtChain = new ResourceFieldChainExpression(archivedAtAttribute); FilterExpression isUnarchived = new ComparisonExpression(ComparisonOperator.Equals, archivedAtChain, new NullConstantExpression()); @@ -64,7 +64,7 @@ private bool IsRequestingCollectionOfTelevisionBroadcasts() { if (_request.IsCollection) { - if (ResourceContext.Equals(_request.PrimaryResource) || ResourceContext.Equals(_request.SecondaryResource)) + if (ResourceType.Equals(_request.PrimaryResourceType) || ResourceType.Equals(_request.SecondaryResourceType)) { return true; } @@ -90,7 +90,7 @@ private bool IsIncludingCollectionOfTelevisionBroadcasts() foreach (IncludeElementExpression includeElement in includeElements) { - if (includeElement.Relationship is HasManyAttribute && includeElement.Relationship.RightType == ResourceContext.ResourceType) + if (includeElement.Relationship is HasManyAttribute && includeElement.Relationship.RightType.Equals(ResourceType)) { return true; } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs index 60dfb891b0..fd9dd6afa3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs @@ -15,10 +15,10 @@ public sealed class LyricRepository : EntityFrameworkCoreRepository public override string TransactionId => _extraDbContext.Database.CurrentTransaction.TransactionId.ToString(); - public LyricRepository(ExtraDbContext extraDbContext, ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, - IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, - IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) + public LyricRepository(ExtraDbContext extraDbContext, ITargetedFields targetedFields, IDbContextResolver dbContextResolver, + IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, + ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { _extraDbContext = extraDbContext; diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs index b379f6614b..b6268ee068 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs @@ -14,10 +14,10 @@ public sealed class MusicTrackRepository : EntityFrameworkCoreRepository null; - public MusicTrackRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + public MusicTrackRepository(ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) + : base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarCompositeKeyAwareRepository.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarCompositeKeyAwareRepository.cs index f76e0baa66..035d18a1c9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarCompositeKeyAwareRepository.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarCompositeKeyAwareRepository.cs @@ -16,10 +16,10 @@ public class CarCompositeKeyAwareRepository : EntityFrameworkCor { private readonly CarExpressionRewriter _writer; - public CarCompositeKeyAwareRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + public CarCompositeKeyAwareRepository(ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) + : base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { _writer = new CarExpressionRewriter(resourceGraph); } @@ -57,10 +57,10 @@ private void RecursiveRewriteFilterInLayer(QueryLayer queryLayer) public class CarCompositeKeyAwareRepository : CarCompositeKeyAwareRepository, IResourceRepository where TResource : class, IIdentifiable { - public CarCompositeKeyAwareRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + public CarCompositeKeyAwareRepository(ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) + : base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs index cce9320cce..c5ec910a98 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs @@ -25,10 +25,10 @@ internal sealed class CarExpressionRewriter : QueryExpressionRewriter public CarExpressionRewriter(IResourceGraph resourceGraph) { - ResourceContext carResourceContext = resourceGraph.GetResourceContext(); + ResourceType carType = resourceGraph.GetResourceType(); - _regionIdAttribute = carResourceContext.GetAttributeByPropertyName(nameof(Car.RegionId)); - _licensePlateAttribute = carResourceContext.GetAttributeByPropertyName(nameof(Car.LicensePlate)); + _regionIdAttribute = carType.GetAttributeByPropertyName(nameof(Car.RegionId)); + _licensePlateAttribute = carType.GetAttributeByPropertyName(nameof(Car.LicensePlate)); } public override QueryExpression VisitComparison(ComparisonExpression expression, object argument) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/BuildingRepository.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/BuildingRepository.cs index 4c49bdc209..4c32d6d3a6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/BuildingRepository.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/BuildingRepository.cs @@ -13,10 +13,10 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.EagerLoading [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class BuildingRepository : EntityFrameworkCoreRepository { - public BuildingRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + public BuildingRepository(ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) + : base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs index 8aa640c530..cc6a6cc372 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs @@ -43,7 +43,7 @@ public async Task Cannot_filter_in_unknown_scope() 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 '{Unknown.Relationship}' does not exist on resource 'webAccounts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'webAccounts'."); error.Source.Parameter.Should().Be($"filter[{Unknown.Relationship}]"); } @@ -64,7 +64,7 @@ public async Task Cannot_filter_in_unknown_nested_scope() 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 '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource 'blogPosts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource type 'blogPosts'."); error.Source.Parameter.Should().Be($"filter[posts.{Unknown.Relationship}]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs index ee19cf83f2..d6efa22782 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs @@ -703,7 +703,7 @@ public async Task Cannot_include_unknown_relationship() 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 '{Unknown.Relationship}' does not exist on resource 'webAccounts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'webAccounts'."); error.Source.Parameter.Should().Be("include"); } @@ -724,7 +724,7 @@ public async Task Cannot_include_unknown_nested_relationship() 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 '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource 'blogPosts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource type 'blogPosts'."); error.Source.Parameter.Should().Be("include"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs index 5fa24d4e7b..5c5eb11f6d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs @@ -405,7 +405,7 @@ public async Task Cannot_paginate_in_unknown_scope() 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 '{Unknown.Relationship}' does not exist on resource 'webAccounts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'webAccounts'."); error.Source.Parameter.Should().Be("page[number]"); } @@ -426,7 +426,7 @@ public async Task Cannot_paginate_in_unknown_nested_scope() 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 '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource 'blogPosts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource type 'blogPosts'."); error.Source.Parameter.Should().Be("page[size]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs index c6ec812da0..c719961d58 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs @@ -467,7 +467,7 @@ public async Task Cannot_sort_in_unknown_scope() 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 '{Unknown.Relationship}' does not exist on resource 'webAccounts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'webAccounts'."); error.Source.Parameter.Should().Be($"sort[{Unknown.Relationship}]"); } @@ -488,7 +488,7 @@ public async Task Cannot_sort_in_unknown_nested_scope() 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 '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource 'blogPosts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource type 'blogPosts'."); error.Source.Parameter.Should().Be($"sort[posts.{Unknown.Relationship}]"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs index 0bf6da70f0..d2c9a3e780 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs @@ -19,10 +19,10 @@ public sealed class ResultCapturingRepository : EntityFrameworkCoreRe { private readonly ResourceCaptureStore _captureStore; - public ResultCapturingRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + public ResultCapturingRepository(ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor, ResourceCaptureStore captureStore) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) + : base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { _captureStore = captureStore; } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs index e3f0dccfe7..13709920ca 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs @@ -35,7 +35,7 @@ public override IImmutableSet OnApplyIncludes(IImmutab return existingIncludes; } - RelationshipAttribute orbitsAroundRelationship = ResourceContext.GetRelationshipByPropertyName(nameof(Moon.OrbitsAround)); + RelationshipAttribute orbitsAroundRelationship = ResourceType.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 512e79556e..820074667c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs @@ -49,7 +49,7 @@ public override FilterExpression OnApplyFilter(FilterExpression existingFilter) if (_clientSettingsProvider.ArePlanetsWithPrivateNameHidden) { - AttrAttribute privateNameAttribute = ResourceContext.GetAttributeByPropertyName(nameof(Planet.PrivateName)); + AttrAttribute privateNameAttribute = ResourceType.GetAttributeByPropertyName(nameof(Planet.PrivateName)); FilterExpression hasNoPrivateName = new ComparisonExpression(ComparisonOperator.Equals, new ResourceFieldChainExpression(privateNameAttribute), new NullConstantExpression()); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion/SoftDeletionAwareResourceService.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion/SoftDeletionAwareResourceService.cs index 68ac5a6942..6c5cd0aa98 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion/SoftDeletionAwareResourceService.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion/SoftDeletionAwareResourceService.cs @@ -41,7 +41,7 @@ public SoftDeletionAwareResourceService(ISystemClock systemClock, ITargetedField public override async Task CreateAsync(TResource resource, CancellationToken cancellationToken) { - if (_targetedFields.Relationships.Any(relationship => IsSoftDeletable(relationship.RightType))) + if (_targetedFields.Relationships.Any(relationship => IsSoftDeletable(relationship.RightType.ClrType))) { await AssertResourcesToAssignInRelationshipsExistAsync(resource, cancellationToken); } @@ -51,7 +51,7 @@ public override async Task CreateAsync(TResource resource, Cancellati public override async Task UpdateAsync(TId id, TResource resource, CancellationToken cancellationToken) { - if (_targetedFields.Relationships.Any(relationship => IsSoftDeletable(relationship.RightType))) + if (_targetedFields.Relationships.Any(relationship => IsSoftDeletable(relationship.RightType.ClrType))) { await AssertResourcesToAssignInRelationshipsExistAsync(resource, cancellationToken); } @@ -61,7 +61,7 @@ public override async Task UpdateAsync(TId id, TResource resource, Ca public override async Task SetRelationshipAsync(TId leftId, string relationshipName, object rightValue, CancellationToken cancellationToken) { - if (IsSoftDeletable(_request.Relationship.RightType)) + if (IsSoftDeletable(_request.Relationship.RightType.ClrType)) { await AssertRightResourcesExistAsync(rightValue, cancellationToken); } @@ -77,7 +77,7 @@ public override async Task AddToToManyRelationshipAsync(TId leftId, string relat _ = await GetPrimaryResourceByIdAsync(leftId, TopFieldSelection.OnlyIdAttribute, cancellationToken); } - if (IsSoftDeletable(_request.Relationship.RightType)) + if (IsSoftDeletable(_request.Relationship.RightType.ClrType)) { await AssertRightResourcesExistAsync(rightResourceIds, cancellationToken); } @@ -107,9 +107,9 @@ private async Task SoftDeleteAsync(TId id, CancellationToken cancellationToken) await _repositoryAccessor.UpdateAsync(resourceFromDatabase, resourceFromDatabase, cancellationToken); } - private static bool IsSoftDeletable(Type resourceType) + private static bool IsSoftDeletable(Type resourceClrType) { - return typeof(ISoftDeletable).IsAssignableFrom(resourceType); + return typeof(ISoftDeletable).IsAssignableFrom(resourceClrType); } } diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs index c3b81522ac..84546eefd0 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs @@ -1,6 +1,5 @@ using System; using FluentAssertions; -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; @@ -53,11 +52,10 @@ public sealed class LinkInclusionTests [InlineData(LinkTypes.All, LinkTypes.Related, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.Paging, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.All, LinkTypes.All)] - public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInResourceContext, LinkTypes linksInOptions, LinkTypes expected) + public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInResourceType, LinkTypes linksInOptions, LinkTypes expected) { // Arrange - var exampleResourceContext = new ResourceContext(nameof(ExampleResource), typeof(ExampleResource), typeof(int), Array.Empty(), - Array.Empty(), Array.Empty(), linksInResourceContext); + var exampleResourceType = new ResourceType(nameof(ExampleResource), typeof(ExampleResource), typeof(int), topLevelLinks: linksInResourceType); var options = new JsonApiOptions { @@ -66,7 +64,7 @@ public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInReso var request = new JsonApiRequest { - PrimaryResource = exampleResourceContext, + PrimaryResourceType = exampleResourceType, PrimaryId = "1", IsCollection = true, Kind = EndpointKind.Relationship, @@ -80,13 +78,10 @@ public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInReso TotalResourceCount = 10 }; - var resourceGraph = new ResourceGraph(exampleResourceContext.AsHashSet()); var httpContextAccessor = new FakeHttpContextAccessor(); var linkGenerator = new FakeLinkGenerator(); var controllerResourceMapping = new FakeControllerResourceMapping(); - - var linkBuilder = new LinkBuilder(options, request, paginationContext, resourceGraph, httpContextAccessor, linkGenerator, - controllerResourceMapping); + var linkBuilder = new LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator, controllerResourceMapping); // Act TopLevelLinks topLevelLinks = linkBuilder.GetTopLevelLinks(); @@ -150,11 +145,10 @@ public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInReso [InlineData(LinkTypes.All, LinkTypes.None, LinkTypes.Self)] [InlineData(LinkTypes.All, LinkTypes.Self, LinkTypes.Self)] [InlineData(LinkTypes.All, LinkTypes.All, LinkTypes.Self)] - public void Applies_cascading_settings_for_resource_links(LinkTypes linksInResourceContext, LinkTypes linksInOptions, LinkTypes expected) + public void Applies_cascading_settings_for_resource_links(LinkTypes linksInResourceType, LinkTypes linksInOptions, LinkTypes expected) { // Arrange - var exampleResourceContext = new ResourceContext(nameof(ExampleResource), typeof(ExampleResource), typeof(int), Array.Empty(), - Array.Empty(), Array.Empty(), resourceLinks: linksInResourceContext); + var exampleResourceType = new ResourceType(nameof(ExampleResource), typeof(ExampleResource), typeof(int), resourceLinks: linksInResourceType); var options = new JsonApiOptions { @@ -163,16 +157,13 @@ public void Applies_cascading_settings_for_resource_links(LinkTypes linksInResou var request = new JsonApiRequest(); var paginationContext = new PaginationContext(); - var resourceGraph = new ResourceGraph(exampleResourceContext.AsHashSet()); var httpContextAccessor = new FakeHttpContextAccessor(); var linkGenerator = new FakeLinkGenerator(); var controllerResourceMapping = new FakeControllerResourceMapping(); - - var linkBuilder = new LinkBuilder(options, request, paginationContext, resourceGraph, httpContextAccessor, linkGenerator, - controllerResourceMapping); + var linkBuilder = new LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator, controllerResourceMapping); // Act - ResourceLinks resourceLinks = linkBuilder.GetResourceLinks(nameof(ExampleResource), "id"); + ResourceLinks resourceLinks = linkBuilder.GetResourceLinks(exampleResourceType, "id"); // Assert if (expected == LinkTypes.Self) @@ -311,12 +302,11 @@ public void Applies_cascading_settings_for_resource_links(LinkTypes linksInResou [InlineData(LinkTypes.All, LinkTypes.All, LinkTypes.Self, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.All, LinkTypes.Related, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.All, LinkTypes.All, LinkTypes.All)] - public void Applies_cascading_settings_for_relationship_links(LinkTypes linksInRelationshipAttribute, LinkTypes linksInResourceContext, + public void Applies_cascading_settings_for_relationship_links(LinkTypes linksInRelationshipAttribute, LinkTypes linksInResourceType, LinkTypes linksInOptions, LinkTypes expected) { // Arrange - var exampleResourceContext = new ResourceContext(nameof(ExampleResource), typeof(ExampleResource), typeof(int), Array.Empty(), - Array.Empty(), Array.Empty(), relationshipLinks: linksInResourceContext); + var exampleResourceType = new ResourceType(nameof(ExampleResource), typeof(ExampleResource), typeof(int), relationshipLinks: linksInResourceType); var options = new JsonApiOptions { @@ -325,17 +315,15 @@ public void Applies_cascading_settings_for_relationship_links(LinkTypes linksInR var request = new JsonApiRequest(); var paginationContext = new PaginationContext(); - var resourceGraph = new ResourceGraph(exampleResourceContext.AsHashSet()); var httpContextAccessor = new FakeHttpContextAccessor(); var linkGenerator = new FakeLinkGenerator(); var controllerResourceMapping = new FakeControllerResourceMapping(); - - var linkBuilder = new LinkBuilder(options, request, paginationContext, resourceGraph, httpContextAccessor, linkGenerator, - controllerResourceMapping); + var linkBuilder = new LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator, controllerResourceMapping); var relationship = new HasOneAttribute { - Links = linksInRelationshipAttribute + Links = linksInRelationshipAttribute, + LeftType = exampleResourceType }; // Act @@ -386,12 +374,12 @@ private sealed class FakeHttpContextAccessor : IHttpContextAccessor private sealed class FakeControllerResourceMapping : IControllerResourceMapping { - public Type GetResourceTypeForController(Type controllerType) + public ResourceType TryGetResourceTypeForController(Type controllerType) { throw new NotImplementedException(); } - public string GetControllerNameForResourceType(Type resourceType) + public string TryGetControllerNameForResourceType(ResourceType resourceType) { return null; } diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/BaseParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/BaseParseTests.cs index 9d55b6fe3c..193a5c1ea9 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/BaseParseTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/BaseParseTests.cs @@ -25,6 +25,7 @@ protected BaseParseTests() .Add() .Add() .Add() + .Add() .Build(); // @formatter:wrap_chained_method_calls restore @@ -32,7 +33,7 @@ protected BaseParseTests() Request = new JsonApiRequest { - PrimaryResource = ResourceGraph.GetResourceContext(), + PrimaryResourceType = ResourceGraph.GetResourceType(), IsCollection = true }; } diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/FilterParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/FilterParseTests.cs index b628a06167..966c6ef1ae 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/FilterParseTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/FilterParseTests.cs @@ -58,14 +58,15 @@ public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, b [Theory] [InlineData("filter[", "equals(caption,'some')", "Field name expected.")] - [InlineData("filter[caption]", "equals(url,'some')", "Relationship 'caption' does not exist on resource 'blogs'.")] - [InlineData("filter[posts.caption]", "equals(firstName,'some')", "Relationship 'caption' in 'posts.caption' does not exist on resource 'blogPosts'.")] + [InlineData("filter[caption]", "equals(url,'some')", "Relationship 'caption' does not exist on resource type 'blogs'.")] + [InlineData("filter[posts.caption]", "equals(firstName,'some')", + "Relationship 'caption' in 'posts.caption' does not exist on resource type 'blogPosts'.")] [InlineData("filter[posts.author]", "equals(firstName,'some')", - "Relationship 'author' in 'posts.author' must be a to-many relationship on resource 'blogPosts'.")] + "Relationship 'author' in 'posts.author' must be a to-many relationship on resource type 'blogPosts'.")] [InlineData("filter[posts.comments.author]", "equals(firstName,'some')", - "Relationship 'author' in 'posts.comments.author' must be a to-many relationship on resource 'comments'.")] - [InlineData("filter[posts]", "equals(author,'some')", "Attribute 'author' does not exist on resource 'blogPosts'.")] - [InlineData("filter[posts]", "lessThan(author,null)", "Attribute 'author' does not exist on resource 'blogPosts'.")] + "Relationship 'author' in 'posts.comments.author' must be a to-many relationship on resource type 'comments'.")] + [InlineData("filter[posts]", "equals(author,'some')", "Attribute 'author' does not exist on resource type 'blogPosts'.")] + [InlineData("filter[posts]", "lessThan(author,null)", "Attribute 'author' does not exist on resource type 'blogPosts'.")] [InlineData("filter", " ", "Unexpected whitespace.")] [InlineData("filter", "contains(owner.displayName, )", "Unexpected whitespace.")] [InlineData("filter", "some", "Filter function expected.")] @@ -76,19 +77,19 @@ public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, b [InlineData("filter", "equals(count(posts),", "Count function, value between quotes, null or field name expected.")] [InlineData("filter", "equals(title,')", "' expected.")] [InlineData("filter", "equals(title,null", ") expected.")] - [InlineData("filter", "equals(null", "Field 'null' does not exist on resource 'blogs'.")] + [InlineData("filter", "equals(null", "Field 'null' does not exist on resource type 'blogs'.")] [InlineData("filter", "equals(title,(", "Count function, value between quotes, null or field name expected.")] - [InlineData("filter", "equals(has(posts),'true')", "Field 'has' does not exist on resource 'blogs'.")] + [InlineData("filter", "equals(has(posts),'true')", "Field 'has' does not exist on resource type 'blogs'.")] [InlineData("filter", "has(posts,", "Filter function expected.")] [InlineData("filter", "contains)", "( expected.")] [InlineData("filter", "contains(title,'a','b')", ") expected.")] [InlineData("filter", "contains(title,null)", "Value between quotes expected.")] - [InlineData("filter[posts]", "contains(author,null)", "Attribute 'author' does not exist on resource 'blogPosts'.")] - [InlineData("filter", "any(null,'a','b')", "Attribute 'null' does not exist on resource 'blogs'.")] + [InlineData("filter[posts]", "contains(author,null)", "Attribute 'author' does not exist on resource type 'blogPosts'.")] + [InlineData("filter", "any(null,'a','b')", "Attribute 'null' does not exist on resource type 'blogs'.")] [InlineData("filter", "any('a','b','c')", "Field name expected.")] [InlineData("filter", "any(title,'b','c',)", "Value between quotes expected.")] [InlineData("filter", "any(title,'b')", ", expected.")] - [InlineData("filter[posts]", "any(author,'a','b')", "Attribute 'author' does not exist on resource 'blogPosts'.")] + [InlineData("filter[posts]", "any(author,'a','b')", "Attribute 'author' does not exist on resource type 'blogPosts'.")] [InlineData("filter", "and(", "Filter function expected.")] [InlineData("filter", "or(equals(title,'some'),equals(title,'other')", ") expected.")] [InlineData("filter", "or(equals(title,'some'),equals(title,'other')))", "End of expression expected.")] diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/IncludeParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/IncludeParseTests.cs index af2ea10a99..afa922ebcc 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/IncludeParseTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/IncludeParseTests.cs @@ -56,9 +56,9 @@ public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, b [InlineData("includes", ",", "Relationship name expected.")] [InlineData("includes", "posts,", "Relationship name expected.")] [InlineData("includes", "posts[", ", expected.")] - [InlineData("includes", "title", "Relationship 'title' does not exist on resource 'blogs'.")] + [InlineData("includes", "title", "Relationship 'title' does not exist on resource type 'blogs'.")] [InlineData("includes", "posts.comments.publishTime,", - "Relationship 'publishTime' in 'posts.comments.publishTime' does not exist on resource 'comments'.")] + "Relationship 'publishTime' in 'posts.comments.publishTime' does not exist on resource type 'comments'.")] public void Reader_Read_Fails(string parameterName, string parameterValue, string errorMessage) { // Act diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/LegacyFilterParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/LegacyFilterParseTests.cs index 1c447a297a..0210db3140 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/LegacyFilterParseTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/LegacyFilterParseTests.cs @@ -22,7 +22,7 @@ public LegacyFilterParseTests() { Options.EnableLegacyFilterNotation = true; - Request.PrimaryResource = ResourceGraph.GetResourceContext(); + Request.PrimaryResourceType = ResourceGraph.GetResourceType(); var resourceFactory = new ResourceFactory(new ServiceContainer()); _reader = new FilterQueryStringParameterReader(Request, ResourceGraph, resourceFactory, Options); @@ -32,15 +32,16 @@ public LegacyFilterParseTests() [InlineData("filter", "some", "Expected field name between brackets in filter parameter name.")] [InlineData("filter[", "some", "Expected field name between brackets in filter parameter name.")] [InlineData("filter[]", "some", "Expected field name between brackets in filter parameter name.")] - [InlineData("filter[.]", "some", "Relationship '' in '.' does not exist on resource 'blogPosts'.")] - [InlineData("filter[some]", "other", "Field 'some' does not exist on resource 'blogPosts'.")] - [InlineData("filter[author]", "some", "Attribute 'author' does not exist on resource 'blogPosts'.")] - [InlineData("filter[author.posts]", "some", "Field 'posts' in 'author.posts' must be an attribute or a to-one relationship on resource 'webAccounts'.")] - [InlineData("filter[unknown.id]", "some", "Relationship 'unknown' in 'unknown.id' does not exist on resource 'blogPosts'.")] - [InlineData("filter", "expr:equals(some,'other')", "Field 'some' does not exist on resource 'blogPosts'.")] - [InlineData("filter", "expr:equals(author,'Joe')", "Attribute 'author' does not exist on resource 'blogPosts'.")] - [InlineData("filter", "expr:has(author)", "Relationship 'author' must be a to-many relationship on resource 'blogPosts'.")] - [InlineData("filter", "expr:equals(count(author),'1')", "Relationship 'author' must be a to-many relationship on resource 'blogPosts'.")] + [InlineData("filter[.]", "some", "Relationship '' in '.' does not exist on resource type 'blogPosts'.")] + [InlineData("filter[some]", "other", "Field 'some' does not exist on resource type 'blogPosts'.")] + [InlineData("filter[author]", "some", "Attribute 'author' does not exist on resource type 'blogPosts'.")] + [InlineData("filter[author.posts]", "some", + "Field 'posts' in 'author.posts' must be an attribute or a to-one relationship on resource type 'webAccounts'.")] + [InlineData("filter[unknown.id]", "some", "Relationship 'unknown' in 'unknown.id' does not exist on resource type 'blogPosts'.")] + [InlineData("filter", "expr:equals(some,'other')", "Field 'some' does not exist on resource type 'blogPosts'.")] + [InlineData("filter", "expr:equals(author,'Joe')", "Attribute 'author' does not exist on resource type 'blogPosts'.")] + [InlineData("filter", "expr:has(author)", "Relationship 'author' must be a to-many relationship on resource type 'blogPosts'.")] + [InlineData("filter", "expr:equals(count(author),'1')", "Relationship 'author' must be a to-many relationship on resource type 'blogPosts'.")] public void Reader_Read_Fails(string parameterName, string parameterValue, string errorMessage) { // Act diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/PaginationParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/PaginationParseTests.cs index 71afbca69f..924569618d 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/PaginationParseTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/PaginationParseTests.cs @@ -66,10 +66,10 @@ public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, b [InlineData("1(", ", expected.")] [InlineData("posts:-abc", "Digits expected.")] [InlineData("posts:-1", "Page number cannot be negative or zero.")] - [InlineData("posts.id", "Relationship 'id' in 'posts.id' does not exist on resource 'blogPosts'.")] - [InlineData("posts.comments.id", "Relationship 'id' in 'posts.comments.id' does not exist on resource 'comments'.")] - [InlineData("posts.author", "Relationship 'author' in 'posts.author' must be a to-many relationship on resource 'blogPosts'.")] - [InlineData("something", "Relationship 'something' does not exist on resource 'blogs'.")] + [InlineData("posts.id", "Relationship 'id' in 'posts.id' does not exist on resource type 'blogPosts'.")] + [InlineData("posts.comments.id", "Relationship 'id' in 'posts.comments.id' does not exist on resource type 'comments'.")] + [InlineData("posts.author", "Relationship 'author' in 'posts.author' must be a to-many relationship on resource type 'blogPosts'.")] + [InlineData("something", "Relationship 'something' does not exist on resource type 'blogs'.")] public void Reader_Read_Page_Number_Fails(string parameterValue, string errorMessage) { // Act @@ -99,10 +99,10 @@ public void Reader_Read_Page_Number_Fails(string parameterValue, string errorMes [InlineData("1(", ", expected.")] [InlineData("posts:-abc", "Digits expected.")] [InlineData("posts:-1", "Page size cannot be negative.")] - [InlineData("posts.id", "Relationship 'id' in 'posts.id' does not exist on resource 'blogPosts'.")] - [InlineData("posts.comments.id", "Relationship 'id' in 'posts.comments.id' does not exist on resource 'comments'.")] - [InlineData("posts.author", "Relationship 'author' in 'posts.author' must be a to-many relationship on resource 'blogPosts'.")] - [InlineData("something", "Relationship 'something' does not exist on resource 'blogs'.")] + [InlineData("posts.id", "Relationship 'id' in 'posts.id' does not exist on resource type 'blogPosts'.")] + [InlineData("posts.comments.id", "Relationship 'id' in 'posts.comments.id' does not exist on resource type 'comments'.")] + [InlineData("posts.author", "Relationship 'author' in 'posts.author' must be a to-many relationship on resource type 'blogPosts'.")] + [InlineData("something", "Relationship 'something' does not exist on resource type 'blogs'.")] public void Reader_Read_Page_Size_Fails(string parameterValue, string errorMessage) { // Act diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/SortParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/SortParseTests.cs index b5776bfbbf..4c4d29acbb 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/SortParseTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/SortParseTests.cs @@ -54,22 +54,23 @@ public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, b [Theory] [InlineData("sort[", "id", "Field name expected.")] - [InlineData("sort[abc.def]", "id", "Relationship 'abc' in 'abc.def' does not exist on resource 'blogs'.")] - [InlineData("sort[posts.author]", "id", "Relationship 'author' in 'posts.author' must be a to-many relationship on resource 'blogPosts'.")] + [InlineData("sort[abc.def]", "id", "Relationship 'abc' in 'abc.def' does not exist on resource type 'blogs'.")] + [InlineData("sort[posts.author]", "id", "Relationship 'author' in 'posts.author' must be a to-many relationship on resource type 'blogPosts'.")] [InlineData("sort", "", "-, count function or field name expected.")] [InlineData("sort", " ", "Unexpected whitespace.")] [InlineData("sort", "-", "Count function or field name expected.")] - [InlineData("sort", "abc", "Attribute 'abc' does not exist on resource 'blogs'.")] - [InlineData("sort[posts]", "author", "Attribute 'author' does not exist on resource 'blogPosts'.")] - [InlineData("sort[posts]", "author.livingAddress", "Attribute 'livingAddress' in 'author.livingAddress' does not exist on resource 'webAccounts'.")] + [InlineData("sort", "abc", "Attribute 'abc' does not exist on resource type 'blogs'.")] + [InlineData("sort[posts]", "author", "Attribute 'author' does not exist on resource type 'blogPosts'.")] + [InlineData("sort[posts]", "author.livingAddress", + "Attribute 'livingAddress' in 'author.livingAddress' does not exist on resource type 'webAccounts'.")] [InlineData("sort", "-count", "( expected.")] [InlineData("sort", "count", "( expected.")] [InlineData("sort", "count(posts", ") expected.")] [InlineData("sort", "count(", "Field name expected.")] [InlineData("sort", "count(-abc)", "Field name expected.")] - [InlineData("sort", "count(abc)", "Relationship 'abc' does not exist on resource 'blogs'.")] - [InlineData("sort", "count(id)", "Relationship 'id' does not exist on resource 'blogs'.")] - [InlineData("sort[posts]", "count(author)", "Relationship 'author' must be a to-many relationship on resource 'blogPosts'.")] + [InlineData("sort", "count(abc)", "Relationship 'abc' does not exist on resource type 'blogs'.")] + [InlineData("sort", "count(id)", "Relationship 'id' does not exist on resource type 'blogs'.")] + [InlineData("sort[posts]", "count(author)", "Relationship 'author' must be a to-many relationship on resource type 'blogPosts'.")] [InlineData("sort[posts]", "caption,", "-, count function or field name expected.")] [InlineData("sort[posts]", "caption:", ", expected.")] [InlineData("sort[posts]", "caption,-", "Count function or field name expected.")] diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/SparseFieldSetParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/SparseFieldSetParseTests.cs index fb017cfd9b..8142160429 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/SparseFieldSetParseTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/SparseFieldSetParseTests.cs @@ -61,11 +61,11 @@ public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, b [InlineData("fields[owner]", "", "Resource type 'owner' does not exist.")] [InlineData("fields[owner.posts]", "id", "Resource type 'owner.posts' does not exist.")] [InlineData("fields[blogPosts]", " ", "Unexpected whitespace.")] - [InlineData("fields[blogPosts]", "some", "Field 'some' does not exist on resource 'blogPosts'.")] - [InlineData("fields[blogPosts]", "id,owner.name", "Field 'owner.name' does not exist on resource 'blogPosts'.")] + [InlineData("fields[blogPosts]", "some", "Field 'some' does not exist on resource type 'blogPosts'.")] + [InlineData("fields[blogPosts]", "id,owner.name", "Field 'owner.name' does not exist on resource type 'blogPosts'.")] [InlineData("fields[blogPosts]", "id(", ", expected.")] [InlineData("fields[blogPosts]", "id,", "Field name expected.")] - [InlineData("fields[blogPosts]", "author.id,", "Field 'author.id' does not exist on resource 'blogPosts'.")] + [InlineData("fields[blogPosts]", "author.id,", "Field 'author.id' does not exist on resource type 'blogPosts'.")] public void Reader_Read_Fails(string parameterName, string parameterValue, string errorMessage) { // Act diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs index 5c63169ae9..5bca89bad2 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/InputConversionTests.cs @@ -25,7 +25,7 @@ public void Converts_various_data_types_with_values() DocumentAdapter documentAdapter = CreateDocumentAdapter(resourceGraph => new JsonApiRequest { Kind = EndpointKind.Primary, - PrimaryResource = resourceGraph.GetResourceContext(), + PrimaryResourceType = resourceGraph.GetResourceType(), WriteOperation = WriteOperationKind.CreateResource }); @@ -145,7 +145,7 @@ public void Converts_various_data_types_with_defaults() DocumentAdapter documentAdapter = CreateDocumentAdapter(resourceGraph => new JsonApiRequest { Kind = EndpointKind.Primary, - PrimaryResource = resourceGraph.GetResourceContext(), + PrimaryResourceType = resourceGraph.GetResourceType(), WriteOperation = WriteOperationKind.CreateResource }); @@ -254,14 +254,14 @@ private static DocumentAdapter CreateDocumentAdapter(Func CreateFactory() public void Dispose() { - RunOnDatabaseAsync(async context => await context.Database.EnsureDeletedAsync()).Wait(); + RunOnDatabaseAsync(async dbContext => await dbContext.Database.EnsureDeletedAsync()).Wait(); Factory.Dispose(); } diff --git a/test/UnitTests/Builders/ResourceGraphBuilderTests.cs b/test/UnitTests/Builders/ResourceGraphBuilderTests.cs index b6cd276c99..90171ad268 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 dbResourceContext = resourceGraph.GetResourceContext("dbResources"); - ResourceContext nonDbResourceContext = resourceGraph.GetResourceContext("nonDbResources"); - Assert.Equal(typeof(DbResource), dbResourceContext.ResourceType); - Assert.Equal(typeof(NonDbResource), nonDbResourceContext.ResourceType); + ResourceType dbResourceType = resourceGraph.GetResourceType("dbResources"); + ResourceType nonDbResourceType = resourceGraph.GetResourceType("nonDbResources"); + Assert.Equal(typeof(DbResource), dbResourceType.ClrType); + Assert.Equal(typeof(NonDbResource), nonDbResourceType.ClrType); } [Fact] @@ -41,13 +41,14 @@ public void Resources_Without_Names_Specified_Will_Use_Configured_Formatter() // Arrange var builder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); builder.Add(); + builder.Add(); // Act IResourceGraph resourceGraph = builder.Build(); // Assert - ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(TestResource)); - Assert.Equal("testResources", resourceContext.PublicName); + ResourceType testResourceType = resourceGraph.GetResourceType(typeof(TestResource)); + Assert.Equal("testResources", testResourceType.PublicName); } [Fact] @@ -56,13 +57,14 @@ public void Attrs_Without_Names_Specified_Will_Use_Configured_Formatter() // Arrange var builder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); builder.Add(); + builder.Add(); // Act IResourceGraph resourceGraph = builder.Build(); // Assert - ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(TestResource)); - Assert.Contains(resourceContext.Attributes, attribute => attribute.PublicName == "compoundAttribute"); + ResourceType testResourceType = resourceGraph.GetResourceType(typeof(TestResource)); + Assert.Contains(testResourceType.Attributes, attribute => attribute.PublicName == "compoundAttribute"); } [Fact] @@ -71,14 +73,15 @@ public void Relationships_Without_Names_Specified_Will_Use_Configured_Formatter( // Arrange var builder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); builder.Add(); + builder.Add(); // Act IResourceGraph resourceGraph = builder.Build(); // Assert - 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); + ResourceType testResourceType = resourceGraph.GetResourceType(typeof(TestResource)); + Assert.Equal("relatedResource", testResourceType.Relationships.Single(relationship => relationship is HasOneAttribute).PublicName); + Assert.Equal("relatedResources", testResourceType.Relationships.Single(relationship => relationship is not HasOneAttribute).PublicName); } private sealed class NonDbResource : Identifiable diff --git a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index 663d166f88..d9d148f46a 100644 --- a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -34,10 +34,10 @@ public void RegisterResource_DeviatingDbContextPropertyName_RegistersCorrectly() ServiceProvider provider = services.BuildServiceProvider(); var resourceGraph = provider.GetRequiredService(); - ResourceContext resourceContext = resourceGraph.GetResourceContext(); + ResourceType personType = resourceGraph.GetResourceType(); // Assert - Assert.Equal("people", resourceContext.PublicName); + Assert.Equal("people", personType.PublicName); } [Fact] @@ -174,8 +174,8 @@ public void AddJsonApi_With_Context_Uses_Resource_Type_Name_If_NoOtherSpecified( ServiceProvider provider = services.BuildServiceProvider(); var resourceGraph = provider.GetRequiredService(); - ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(IntResource)); - Assert.Equal("intResources", resourceContext.PublicName); + ResourceType intResourceType = resourceGraph.GetResourceType(typeof(IntResource)); + Assert.Equal("intResources", intResourceType.PublicName); } private sealed class IntResource : Identifiable diff --git a/test/UnitTests/Graph/ResourceDescriptorAssemblyCacheTests.cs b/test/UnitTests/Graph/ResourceDescriptorAssemblyCacheTests.cs index 99e8f54931..c602c368a0 100644 --- a/test/UnitTests/Graph/ResourceDescriptorAssemblyCacheTests.cs +++ b/test/UnitTests/Graph/ResourceDescriptorAssemblyCacheTests.cs @@ -14,34 +14,34 @@ public sealed class ResourceDescriptorAssemblyCacheTests public void GetResourceDescriptorsPerAssembly_Locates_Identifiable_Resource() { // Arrange - Type resourceType = typeof(Model); + Type resourceClrType = typeof(Model); var assemblyCache = new ResourceDescriptorAssemblyCache(); - assemblyCache.RegisterAssembly(resourceType.Assembly); + assemblyCache.RegisterAssembly(resourceClrType.Assembly); // Act IReadOnlyCollection descriptors = assemblyCache.GetResourceDescriptors(); // Assert descriptors.Should().NotBeEmpty(); - descriptors.Should().ContainSingle(descriptor => descriptor.ResourceType == resourceType); + descriptors.Should().ContainSingle(descriptor => descriptor.ResourceClrType == resourceClrType); } [Fact] public void GetResourceDescriptorsPerAssembly_Only_Contains_IIdentifiable_Types() { // Arrange - Type resourceType = typeof(Model); + Type resourceClrType = typeof(Model); var assemblyCache = new ResourceDescriptorAssemblyCache(); - assemblyCache.RegisterAssembly(resourceType.Assembly); + assemblyCache.RegisterAssembly(resourceClrType.Assembly); // Act IReadOnlyCollection descriptors = assemblyCache.GetResourceDescriptors(); // Assert descriptors.Should().NotBeEmpty(); - descriptors.Select(descriptor => descriptor.ResourceType).Should().AllBeAssignableTo(); + descriptors.Select(descriptor => descriptor.ResourceClrType).Should().AllBeAssignableTo(); } } } diff --git a/test/UnitTests/Graph/TypeLocatorTests.cs b/test/UnitTests/Graph/TypeLocatorTests.cs index 0d44732a2a..4ff9625ebc 100644 --- a/test/UnitTests/Graph/TypeLocatorTests.cs +++ b/test/UnitTests/Graph/TypeLocatorTests.cs @@ -85,29 +85,29 @@ public void GetIdType_Correctly_Identifies_NonJsonApiResource() public void TryGetResourceDescriptor_Returns_Type_If_Type_Is_IIdentifiable() { // Arrange - Type resourceType = typeof(Model); + Type resourceClrType = typeof(Model); var typeLocator = new TypeLocator(); // Act - ResourceDescriptor descriptor = typeLocator.TryGetResourceDescriptor(resourceType); + ResourceDescriptor descriptor = typeLocator.TryGetResourceDescriptor(resourceClrType); // Assert Assert.NotNull(descriptor); - Assert.Equal(resourceType, descriptor.ResourceType); - Assert.Equal(typeof(int), descriptor.IdType); + Assert.Equal(resourceClrType, descriptor.ResourceClrType); + Assert.Equal(typeof(int), descriptor.IdClrType); } [Fact] public void TryGetResourceDescriptor_Returns_False_If_Type_Is_IIdentifiable() { // Arrange - Type resourceType = typeof(string); + Type resourceClrType = typeof(string); var typeLocator = new TypeLocator(); // Act - ResourceDescriptor descriptor = typeLocator.TryGetResourceDescriptor(resourceType); + ResourceDescriptor descriptor = typeLocator.TryGetResourceDescriptor(resourceClrType); // Assert Assert.Null(descriptor); diff --git a/test/UnitTests/Internal/ResourceGraphBuilderTests.cs b/test/UnitTests/Internal/ResourceGraphBuilderTests.cs index d1df3947d8..110aa17027 100644 --- a/test/UnitTests/Internal/ResourceGraphBuilderTests.cs +++ b/test/UnitTests/Internal/ResourceGraphBuilderTests.cs @@ -24,7 +24,7 @@ public void AddDbContext_Does_Not_Throw_If_Context_Contains_Members_That_Do_Not_ var resourceGraph = (ResourceGraph)resourceGraphBuilder.Build(); // Assert - Assert.Empty(resourceGraph.GetResourceContexts()); + Assert.Empty(resourceGraph.GetResourceTypes()); } [Fact] @@ -47,7 +47,7 @@ public void Adding_DbContext_Members_That_Do_Not_Implement_IIdentifiable_Logs_Wa } [Fact] - public void GetResourceContext_Yields_Right_Type_For_LazyLoadingProxy() + public void GetResourceType_Yields_Right_Type_For_LazyLoadingProxy() { // Arrange var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); @@ -57,14 +57,14 @@ public void GetResourceContext_Yields_Right_Type_For_LazyLoadingProxy() // Act var proxy = proxyGenerator.CreateClassProxy(); - ResourceContext resourceContext = resourceGraph.GetResourceContext(proxy.GetType()); + ResourceType barType = resourceGraph.GetResourceType(proxy.GetType()); // Assert - Assert.Equal(typeof(Bar), resourceContext.ResourceType); + Assert.Equal(typeof(Bar), barType.ClrType); } [Fact] - public void GetResourceContext_Yields_Right_Type_For_Identifiable() + public void GetResourceType_Yields_Right_Type_For_Identifiable() { // Arrange var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); @@ -72,10 +72,10 @@ public void GetResourceContext_Yields_Right_Type_For_Identifiable() var resourceGraph = (ResourceGraph)resourceGraphBuilder.Build(); // Act - ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(Bar)); + ResourceType barType = resourceGraph.GetResourceType(typeof(Bar)); // Assert - Assert.Equal(typeof(Bar), resourceContext.ResourceType); + Assert.Equal(typeof(Bar), barType.ClrType); } [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] diff --git a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs index 93122f9a99..244db92b03 100644 --- a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs +++ b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Moq.Language; using Xunit; namespace UnitTests.Middleware @@ -78,11 +77,11 @@ public async Task ParseUrlBase_UrlHasNegativePrimaryIdAndTypeIsInt_ShouldNotThro private Task RunMiddlewareTask(InvokeConfiguration holder) { IControllerResourceMapping controllerResourceMapping = holder.ControllerResourceMapping.Object; - HttpContext context = holder.HttpContext; + HttpContext httpContext = holder.HttpContext; IJsonApiOptions options = holder.Options; JsonApiRequest request = holder.Request; - IResourceGraph resourceGraph = holder.ResourceGraph.Object; - return holder.MiddleWare.InvokeAsync(context, controllerResourceMapping, options, request, resourceGraph, NullLogger.Instance); + + return holder.MiddleWare.InvokeAsync(httpContext, controllerResourceMapping, options, request, NullLogger.Instance); } private InvokeConfiguration GetConfiguration(string path, string resourceName = "users", string action = "", string id = null, Type relType = null) @@ -99,21 +98,18 @@ private InvokeConfiguration GetConfiguration(string path, string resourceName = const string forcedNamespace = "api/v1"; var mockMapping = new Mock(); - mockMapping.Setup(mapping => mapping.GetResourceTypeForController(It.IsAny())).Returns(typeof(string)); + var resourceType = new ResourceType(resourceName, typeof(object), typeof(string)); + mockMapping.Setup(mapping => mapping.TryGetResourceTypeForController(It.IsAny())).Returns(resourceType); IJsonApiOptions options = CreateOptions(forcedNamespace); - Mock mockGraph = CreateMockResourceGraph(resourceName, relType != null); var request = new JsonApiRequest(); if (relType != null) { - request.Relationship = new HasManyAttribute - { - RightType = relType - }; + request.Relationship = new HasManyAttribute(); } - DefaultHttpContext context = CreateHttpContext(path, relType != null, action, id); + DefaultHttpContext httpContext = CreateHttpContext(path, relType != null, action, id); return new InvokeConfiguration { @@ -121,8 +117,7 @@ private InvokeConfiguration GetConfiguration(string path, string resourceName = ControllerResourceMapping = mockMapping, Options = options, Request = request, - HttpContext = context, - ResourceGraph = mockGraph + HttpContext = httpContext }; } @@ -138,9 +133,17 @@ private static IJsonApiOptions CreateOptions(string forcedNamespace) private static DefaultHttpContext CreateHttpContext(string path, bool isRelationship = false, string action = "", string id = null) { - var context = new DefaultHttpContext(); - context.Request.Path = new PathString(path); - context.Response.Body = new MemoryStream(); + var httpContext = new DefaultHttpContext + { + Request = + { + Path = new PathString(path) + }, + Response = + { + Body = new MemoryStream() + } + }; var feature = new RouteValuesFeature { @@ -156,36 +159,15 @@ private static DefaultHttpContext CreateHttpContext(string path, bool isRelation feature.RouteValues["id"] = id; } - context.Features.Set(feature); + httpContext.Features.Set(feature); var controllerActionDescriptor = new ControllerActionDescriptor { ControllerTypeInfo = (TypeInfo)typeof(object) }; - context.SetEndpoint(new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(controllerActionDescriptor), null)); - return context; - } - - private Mock CreateMockResourceGraph(string resourceName, bool includeRelationship = false) - { - var mockGraph = new Mock(); - - var resourceContext = new ResourceContext(resourceName, typeof(object), typeof(string), Array.Empty(), - Array.Empty(), Array.Empty()); - - ISetupSequentialResult seq = mockGraph.SetupSequence(resourceGraph => resourceGraph.GetResourceContext(It.IsAny())) - .Returns(resourceContext); - - if (includeRelationship) - { - var relatedContext = new ResourceContext("todoItems", typeof(object), typeof(string), Array.Empty(), - Array.Empty(), Array.Empty()); - - seq.Returns(relatedContext); - } - - return mockGraph; + httpContext.SetEndpoint(new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(controllerActionDescriptor), null)); + return httpContext; } private sealed class InvokeConfiguration @@ -195,7 +177,6 @@ private sealed class InvokeConfiguration public Mock ControllerResourceMapping { 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 bb6a230fd7..a17bfaf0a5 100644 --- a/test/UnitTests/Middleware/JsonApiRequestTests.cs +++ b/test/UnitTests/Middleware/JsonApiRequestTests.cs @@ -58,7 +58,8 @@ public async Task Sets_request_properties_correctly(string requestMethod, string var controllerResourceMappingMock = new Mock(); - controllerResourceMappingMock.Setup(mapping => mapping.GetResourceTypeForController(It.IsAny())).Returns(typeof(TodoItem)); + ResourceType todoItemType = resourceGraph.GetResourceType(); + controllerResourceMappingMock.Setup(mapping => mapping.TryGetResourceTypeForController(It.IsAny())).Returns(todoItemType); var httpContext = new DefaultHttpContext(); SetupRoutes(httpContext, requestMethod, requestPath); @@ -68,16 +69,15 @@ public async Task Sets_request_properties_correctly(string requestMethod, string var middleware = new JsonApiMiddleware(_ => Task.CompletedTask, new HttpContextAccessor()); // Act - await middleware.InvokeAsync(httpContext, controllerResourceMappingMock.Object, options, request, resourceGraph, - NullLogger.Instance); + await middleware.InvokeAsync(httpContext, controllerResourceMappingMock.Object, options, request, NullLogger.Instance); // Assert request.IsCollection.Should().Be(expectIsCollection); request.Kind.Should().Be(expectKind); request.WriteOperation.Should().Be(expectWriteOperation); request.IsReadOnly.Should().Be(expectIsReadOnly); - request.PrimaryResource.Should().NotBeNull(); - request.PrimaryResource.PublicName.Should().Be("todoItems"); + request.PrimaryResourceType.Should().NotBeNull(); + request.PrimaryResourceType.PublicName.Should().Be("todoItems"); } private static void SetupRoutes(HttpContext httpContext, string requestMethod, string requestPath) From f7e7da40a144459270bc46959701d8cb665db1e8 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 8 Oct 2021 12:42:40 +0200 Subject: [PATCH 41/49] Moved logic to build resource graph from DbContext into ResourceGraphBuilder for easier reuse. --- .../JsonApiApplicationBuilder.cs | 22 +--------------- .../Configuration/ResourceGraphBuilder.cs | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 2a128c6266..8db88c4ee2 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -19,8 +19,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -83,7 +81,7 @@ public void AddResourceGraph(ICollection dbContextTypes, Action(); } - private void AddResourcesFromDbContext(DbContext dbContext, ResourceGraphBuilder builder) - { - foreach (IEntityType entityType in dbContext.Model.GetEntityTypes()) - { - if (!IsImplicitManyToManyJoinEntity(entityType)) - { - builder.Add(entityType.ClrType); - } - } - } - - private static bool IsImplicitManyToManyJoinEntity(IEntityType entityType) - { -#pragma warning disable EF1001 // Internal EF Core API usage. - return entityType is EntityType { IsImplicitlyCreatedJoinEntityType: true }; -#pragma warning restore EF1001 // Internal EF Core API usage. - } - public void Dispose() { _intermediateProvider.Dispose(); diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index c4e29e9ae2..c063444d88 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -5,6 +5,9 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Configuration @@ -45,6 +48,28 @@ public IResourceGraph Build() return resourceGraph; } + public ResourceGraphBuilder Add(DbContext dbContext) + { + ArgumentGuard.NotNull(dbContext, nameof(dbContext)); + + foreach (IEntityType entityType in dbContext.Model.GetEntityTypes()) + { + if (!IsImplicitManyToManyJoinEntity(entityType)) + { + Add(entityType.ClrType); + } + } + + return this; + } + + private static bool IsImplicitManyToManyJoinEntity(IEntityType entityType) + { +#pragma warning disable EF1001 // Internal EF Core API usage. + return entityType is EntityType { IsImplicitlyCreatedJoinEntityType: true }; +#pragma warning restore EF1001 // Internal EF Core API usage. + } + /// /// Adds a JSON:API resource with int as the identifier CLR type. /// From 3d98057253dc49ccc3267d342f56f549f3f820a5 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 8 Oct 2021 12:54:45 +0200 Subject: [PATCH 42/49] Opened up ResponseModelAdapter for extensibility --- .../Response/IncludedCollection.cs | 78 ++++++++++++++++ .../Response/ResponseModelAdapter.cs | 89 +++---------------- 2 files changed, 89 insertions(+), 78 deletions(-) create mode 100644 src/JsonApiDotNetCore/Serialization/Response/IncludedCollection.cs diff --git a/src/JsonApiDotNetCore/Serialization/Response/IncludedCollection.cs b/src/JsonApiDotNetCore/Serialization/Response/IncludedCollection.cs new file mode 100644 index 0000000000..46f30c256b --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Response/IncludedCollection.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.Response +{ + [PublicAPI] + public sealed class IncludedCollection + { + private readonly List _includes = new(); + private readonly Dictionary _resourceToIncludeIndexMap = new(IdentifiableComparer.Instance); + + public IList ResourceObjects => _includes; + + public ResourceObject AddOrUpdate(IIdentifiable resource, ResourceObject resourceObject) + { + if (!_resourceToIncludeIndexMap.ContainsKey(resource)) + { + _includes.Add(resourceObject); + _resourceToIncludeIndexMap.Add(resource, _includes.Count - 1); + } + else + { + if (resourceObject.Type != null) + { + int existingIndex = _resourceToIncludeIndexMap[resource]; + ResourceObject existingVersion = _includes[existingIndex]; + + if (existingVersion != resourceObject) + { + MergeRelationships(resourceObject, existingVersion); + + return existingVersion; + } + } + } + + return resourceObject; + } + + private static void MergeRelationships(ResourceObject incomingVersion, ResourceObject existingVersion) + { + // The code below handles the case where one resource is added through different include chains with different relationships. + // We enrich the existing resource object with the added relationships coming from the second chain, to ensure correct resource linkage. + // + // This is best explained using an example. Consider the next inclusion chains: + // + // 1. reviewer.loginAttempts + // 2. author.preferences + // + // Where the relationships `reviewer` and `author` are of the same resource type `people`. Then the next rules apply: + // + // A. People that were included as reviewers from inclusion chain (1) should come with their `loginAttempts` included, but not those from chain (2). + // B. People that were included as authors from inclusion chain (2) should come with their `preferences` included, but not those from chain (1). + // C. For a person that was included as both an reviewer and author (i.e. targeted by both chains), both `loginAttempts` and `preferences` need + // to be present. + // + // For rule (C), the related resources will be included as usual, but we need to fix resource linkage here by merging the relationship objects. + // + // Note that this implementation breaks the overall depth-first ordering of included objects. So solve that, we'd need to use a dependency graph + // for included objects instead of a flat list, which may affect performance. Since the ordering is not guaranteed anyway, keeping it simple for now. + + foreach ((string relationshipName, RelationshipObject relationshipObject) in existingVersion.Relationships.EmptyIfNull()) + { + if (!relationshipObject.Data.IsAssigned) + { + SingleOrManyData incomingRelationshipData = incomingVersion.Relationships[relationshipName].Data; + + if (incomingRelationshipData.IsAssigned) + { + relationshipObject.Data = incomingRelationshipData; + } + } + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs index a8a0c7cff7..f61a6e7aa5 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Linq; using System.Text.Json.Serialization; +using JetBrains.Annotations; using JsonApiDotNetCore.AtomicOperations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; @@ -17,7 +18,8 @@ namespace JsonApiDotNetCore.Serialization.Response { /// - public sealed class ResponseModelAdapter : IResponseModelAdapter + [PublicAPI] + public class ResponseModelAdapter : IResponseModelAdapter { private static readonly CollectionConverter CollectionConverter = new(); @@ -108,7 +110,7 @@ public Document Convert(object model) return document; } - private AtomicResultObject ConvertOperation(OperationContainer operation, IImmutableSet includeElements, + protected virtual AtomicResultObject ConvertOperation(OperationContainer operation, IImmutableSet includeElements, IncludedCollection includedCollection) { ResourceObject resourceObject = null; @@ -129,7 +131,7 @@ private AtomicResultObject ConvertOperation(OperationContainer operation, IImmut }; } - private ResourceObject ConvertResource(IIdentifiable resource, ResourceType resourceType, EndpointKind requestKind, + protected virtual ResourceObject ConvertResource(IIdentifiable resource, ResourceType resourceType, EndpointKind requestKind, IImmutableSet includeElements, IncludedCollection includedCollection, bool isInclude) { IImmutableSet fieldSet = null; @@ -160,7 +162,8 @@ private ResourceObject ConvertResource(IIdentifiable resource, ResourceType reso return resourceObject; } - private IDictionary ConvertAttributes(IIdentifiable resource, ResourceType resourceType, IImmutableSet fieldSet) + protected virtual IDictionary ConvertAttributes(IIdentifiable resource, ResourceType resourceType, + IImmutableSet fieldSet) { if (fieldSet != null) { @@ -198,7 +201,7 @@ private IDictionary ConvertAttributes(IIdentifiable resource, Re return null; } - private IDictionary ConvertRelationships(IIdentifiable resource, ResourceType resourceType, + protected virtual IDictionary ConvertRelationships(IIdentifiable resource, ResourceType resourceType, IImmutableSet fieldSet, EndpointKind requestKind, IImmutableSet includeElements, IncludedCollection includedCollection) { @@ -244,7 +247,7 @@ private static TSource GetFirstOrDefault(IEnumerable return default; } - private RelationshipObject ConvertRelationship(RelationshipAttribute relationship, IIdentifiable leftResource, EndpointKind requestKind, + protected virtual RelationshipObject ConvertRelationship(RelationshipAttribute relationship, IIdentifiable leftResource, EndpointKind requestKind, IncludeElementExpression includeElement, IncludedCollection includedCollection) { SingleOrManyData data = default; @@ -288,7 +291,7 @@ private RelationshipObject ConvertRelationship(RelationshipAttribute relationshi }; } - private JsonApiObject GetApiObject() + protected virtual JsonApiObject GetApiObject() { if (!_options.IncludeJsonApiVersion) { @@ -311,7 +314,7 @@ private JsonApiObject GetApiObject() return jsonApiObject; } - private IList GetIncluded(IncludedCollection includedCollection) + protected virtual IList GetIncluded(IncludedCollection includedCollection) { if (includedCollection.ResourceObjects.Any()) { @@ -320,75 +323,5 @@ private IList GetIncluded(IncludedCollection includedCollection) return _requestQueryStringAccessor.Query.ContainsKey("include") ? Array.Empty() : null; } - - private sealed class IncludedCollection - { - private readonly List _includes = new(); - private readonly Dictionary _resourceToIncludeIndexMap = new(IdentifiableComparer.Instance); - - public IList ResourceObjects => _includes; - - public ResourceObject AddOrUpdate(IIdentifiable resource, ResourceObject resourceObject) - { - if (!_resourceToIncludeIndexMap.ContainsKey(resource)) - { - _includes.Add(resourceObject); - _resourceToIncludeIndexMap.Add(resource, _includes.Count - 1); - } - else - { - if (resourceObject.Type != null) - { - int existingIndex = _resourceToIncludeIndexMap[resource]; - ResourceObject existingVersion = _includes[existingIndex]; - - if (existingVersion != resourceObject) - { - MergeRelationships(resourceObject, existingVersion); - - return existingVersion; - } - } - } - - return resourceObject; - } - - private static void MergeRelationships(ResourceObject incomingVersion, ResourceObject existingVersion) - { - // The code below handles the case where one resource is added through different include chains with different relationships. - // We enrich the existing resource object with the added relationships coming from the second chain, to ensure correct resource linkage. - // - // This is best explained using an example. Consider the next inclusion chains: - // - // 1. reviewer.loginAttempts - // 2. author.preferences - // - // Where the relationships `reviewer` and `author` are of the same resource type `people`. Then the next rules apply: - // - // A. People that were included as reviewers from inclusion chain (1) should come with their `loginAttempts` included, but not those from chain (2). - // B. People that were included as authors from inclusion chain (2) should come with their `preferences` included, but not those from chain (1). - // C. For a person that was included as both an reviewer and author (i.e. targeted by both chains), both `loginAttempts` and `preferences` need - // to be present. - // - // For rule (C), the related resources will be included as usual, but we need to fix resource linkage here by merging the relationship objects. - // - // Note that this implementation breaks the overall depth-first ordering of included objects. So solve that, we'd need to use a dependency graph - // for included objects instead of a flat list, which may affect performance. Since the ordering is not guaranteed anyway, keeping it simple for now. - - foreach ((string relationshipName, RelationshipObject relationshipObject) in existingVersion.Relationships.EmptyIfNull()) - { - if (!relationshipObject.Data.IsAssigned) - { - SingleOrManyData incomingRelationshipData = incomingVersion.Relationships[relationshipName].Data; - - if (incomingRelationshipData.IsAssigned) - { - relationshipObject.Data = incomingRelationshipData; - } - } - } - } - } } } From 2e3659fc6fe74f4792fa485382729d10185999a2 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 11 Oct 2021 11:45:46 +0200 Subject: [PATCH 43/49] Check off roadmap entry --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 1c15a33b2b..b3eb75daa6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -20,7 +20,7 @@ The need for breaking changes has blocked several efforts in the v4.x release, s - [x] Instrumentation [#1032](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1032) - [x] Optimized delete to-many [#1030](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1030) - [x] Support System.Text.Json [#664](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/664) [#999](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/999) [1077](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1077) [1078](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1078) -- [ ] Optimize IIdentifiable to ResourceObject conversion [#1028](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1028) [#1024](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1024) [#233](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/233) +- [x] 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 2c4e75c8479d7521f3389573754bd9a4aeba94ec Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 20 Oct 2021 11:48:39 +0200 Subject: [PATCH 44/49] Review feedback --- .../Configuration/JsonApiValidationFilter.cs | 4 ++-- .../Serialization/Request/Adapters/JsonElementConstraint.cs | 4 ++-- .../Serialization/Request/Adapters/ResourceDataAdapter.cs | 2 +- .../Serialization/Request/Adapters/ResourceIdentityAdapter.cs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs index c78a2936dc..6829e35788 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs @@ -25,7 +25,7 @@ public JsonApiValidationFilter(IHttpContextAccessor httpContextAccessor) /// public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry) { - IServiceProvider serviceProvider = GetServiceProvider(); + IServiceProvider serviceProvider = GetScopedServiceProvider(); var request = serviceProvider.GetRequiredService(); @@ -50,7 +50,7 @@ public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEnt return true; } - private IServiceProvider GetServiceProvider() + private IServiceProvider GetScopedServiceProvider() { HttpContext httpContext = _httpContextAccessor.HttpContext; diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/JsonElementConstraint.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/JsonElementConstraint.cs index 5cb4708fd3..ebdd76945a 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/JsonElementConstraint.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/JsonElementConstraint.cs @@ -9,12 +9,12 @@ namespace JsonApiDotNetCore.Serialization.Request.Adapters public enum JsonElementConstraint { /// - /// A value for the field is not allowed. + /// A value for the element is not allowed. /// Forbidden, /// - /// A value for the field is required. + /// A value for the element is required. /// Required } diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataAdapter.cs index be1fbb1854..f1747fffc8 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataAdapter.cs @@ -33,7 +33,7 @@ public IIdentifiable Convert(SingleOrManyData data, ResourceIden (IIdentifiable resource, ResourceType _) = ConvertResourceObject(data, requirements, state); - // Ensure that IResourceDefinition extensibility point sees the current operation, it case it injects IJsonApiRequest. + // Ensure that IResourceDefinition extensibility point sees the current operation, in case it injects IJsonApiRequest. state.RefreshInjectables(); _resourceDefinitionAccessor.OnDeserialize(resource); diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs index 618feab797..5bb2a52d53 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs @@ -32,13 +32,13 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory ArgumentGuard.NotNull(requirements, nameof(requirements)); ArgumentGuard.NotNull(state, nameof(state)); - ResourceType resourceType = ConvertType(identity, requirements, state); + ResourceType resourceType = ResolveType(identity, requirements, state); IIdentifiable resource = CreateResource(identity, requirements, resourceType.ClrType, state); return (resource, resourceType); } - private ResourceType ConvertType(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) + private ResourceType ResolveType(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) { AssertHasType(identity, state); From 71585955aea237e43944136218181f78baf86b42 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 25 Oct 2021 11:16:58 +0200 Subject: [PATCH 45/49] Simplified existing tests --- .../Serialization/SerializationTests.cs | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs index 98d412ebff..f179073b64 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; @@ -91,13 +90,13 @@ public async Task Returns_no_body_for_failed_HEAD_request() public async Task Can_get_primary_resources_with_include() { // Arrange - List meetings = _fakers.Meeting.Generate(1); - meetings[0].Attendees = _fakers.MeetingAttendee.Generate(1); + Meeting meeting = _fakers.Meeting.Generate(); + meeting.Attendees = _fakers.MeetingAttendee.Generate(1); await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); - dbContext.Meetings.AddRange(meetings); + dbContext.Meetings.Add(meeting); await dbContext.SaveChangesAsync(); }); @@ -117,52 +116,52 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""data"": [ { ""type"": ""meetings"", - ""id"": """ + meetings[0].StringId + @""", + ""id"": """ + meeting.StringId + @""", ""attributes"": { - ""title"": """ + meetings[0].Title + @""", - ""startTime"": """ + meetings[0].StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", - ""duration"": """ + meetings[0].Duration + @""", + ""title"": """ + meeting.Title + @""", + ""startTime"": """ + meeting.StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", + ""duration"": """ + meeting.Duration + @""", ""location"": { - ""lat"": " + meetings[0].Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", - ""lng"": " + meetings[0].Location.Longitude.ToString(CultureInfo.InvariantCulture) + @" + ""lat"": " + meeting.Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", + ""lng"": " + meeting.Location.Longitude.ToString(CultureInfo.InvariantCulture) + @" } }, ""relationships"": { ""attendees"": { ""links"": { - ""self"": ""http://localhost/meetings/" + meetings[0].StringId + @"/relationships/attendees"", - ""related"": ""http://localhost/meetings/" + meetings[0].StringId + @"/attendees"" + ""self"": ""http://localhost/meetings/" + meeting.StringId + @"/relationships/attendees"", + ""related"": ""http://localhost/meetings/" + meeting.StringId + @"/attendees"" }, ""data"": [ { ""type"": ""meetingAttendees"", - ""id"": """ + meetings[0].Attendees[0].StringId + @""" + ""id"": """ + meeting.Attendees[0].StringId + @""" } ] } }, ""links"": { - ""self"": ""http://localhost/meetings/" + meetings[0].StringId + @""" + ""self"": ""http://localhost/meetings/" + meeting.StringId + @""" } } ], ""included"": [ { ""type"": ""meetingAttendees"", - ""id"": """ + meetings[0].Attendees[0].StringId + @""", + ""id"": """ + meeting.Attendees[0].StringId + @""", ""attributes"": { - ""displayName"": """ + meetings[0].Attendees[0].DisplayName + @""" + ""displayName"": """ + meeting.Attendees[0].DisplayName + @""" }, ""relationships"": { ""meeting"": { ""links"": { - ""self"": ""http://localhost/meetingAttendees/" + meetings[0].Attendees[0].StringId + @"/relationships/meeting"", - ""related"": ""http://localhost/meetingAttendees/" + meetings[0].Attendees[0].StringId + @"/meeting"" + ""self"": ""http://localhost/meetingAttendees/" + meeting.Attendees[0].StringId + @"/relationships/meeting"", + ""related"": ""http://localhost/meetingAttendees/" + meeting.Attendees[0].StringId + @"/meeting"" } } }, ""links"": { - ""self"": ""http://localhost/meetingAttendees/" + meetings[0].Attendees[0].StringId + @""" + ""self"": ""http://localhost/meetingAttendees/" + meeting.Attendees[0].StringId + @""" } } ] @@ -173,12 +172,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_get_primary_resources_with_empty_include() { // Arrange - List meetings = _fakers.Meeting.Generate(1); + Meeting meeting = _fakers.Meeting.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); - dbContext.Meetings.AddRange(meetings); + dbContext.Meetings.Add(meeting); await dbContext.SaveChangesAsync(); }); @@ -198,27 +197,27 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""data"": [ { ""type"": ""meetings"", - ""id"": """ + meetings[0].StringId + @""", + ""id"": """ + meeting.StringId + @""", ""attributes"": { - ""title"": """ + meetings[0].Title + @""", - ""startTime"": """ + meetings[0].StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", - ""duration"": """ + meetings[0].Duration + @""", + ""title"": """ + meeting.Title + @""", + ""startTime"": """ + meeting.StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", + ""duration"": """ + meeting.Duration + @""", ""location"": { - ""lat"": " + meetings[0].Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", - ""lng"": " + meetings[0].Location.Longitude.ToString(CultureInfo.InvariantCulture) + @" + ""lat"": " + meeting.Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", + ""lng"": " + meeting.Location.Longitude.ToString(CultureInfo.InvariantCulture) + @" } }, ""relationships"": { ""attendees"": { ""links"": { - ""self"": ""http://localhost/meetings/" + meetings[0].StringId + @"/relationships/attendees"", - ""related"": ""http://localhost/meetings/" + meetings[0].StringId + @"/attendees"" + ""self"": ""http://localhost/meetings/" + meeting.StringId + @"/relationships/attendees"", + ""related"": ""http://localhost/meetings/" + meeting.StringId + @"/attendees"" }, ""data"": [] } }, ""links"": { - ""self"": ""http://localhost/meetings/" + meetings[0].StringId + @""" + ""self"": ""http://localhost/meetings/" + meeting.StringId + @""" } } ], From 7c6684f827d54b4204d32cf969fe1829927e0a28 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 25 Oct 2021 14:28:50 +0200 Subject: [PATCH 46/49] Added extra test for data:null in relationship --- .../Serialization/SerializationTests.cs | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs index f179073b64..42817f2798 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs @@ -169,7 +169,54 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Can_get_primary_resources_with_empty_include() + public async Task Can_get_primary_resource_with_empty_ToOne_include() + { + // Arrange + MeetingAttendee attendee = _fakers.MeetingAttendee.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Attendees.Add(attendee); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/meetingAttendees/{attendee.StringId}?include=meeting"; + + // Act + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Should().BeJson(@"{ + ""links"": { + ""self"": ""http://localhost/meetingAttendees/" + attendee.StringId + @"?include=meeting"" + }, + ""data"": { + ""type"": ""meetingAttendees"", + ""id"": """ + attendee.StringId + @""", + ""attributes"": { + ""displayName"": """ + attendee.DisplayName + @""" + }, + ""relationships"": { + ""meeting"": { + ""links"": { + ""self"": ""http://localhost/meetingAttendees/" + attendee.StringId + @"/relationships/meeting"", + ""related"": ""http://localhost/meetingAttendees/" + attendee.StringId + @"/meeting"" + }, + ""data"": null + } + }, + ""links"": { + ""self"": ""http://localhost/meetingAttendees/" + attendee.StringId + @""" + } + }, + ""included"": [] +}"); + } + + [Fact] + public async Task Can_get_primary_resources_with_empty_ToMany_include() { // Arrange Meeting meeting = _fakers.Meeting.Generate(); From 4a8bfdd5207ff66d18b84d1bbbd984d8f4ee19f0 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 25 Oct 2021 16:09:23 +0200 Subject: [PATCH 47/49] Added test for broken resource linkage --- .../SparseFieldSets/SparseFieldSetTests.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs index 1ce5469a7b..c393d8197b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs @@ -727,5 +727,34 @@ await _testContext.RunOnDatabaseAsync(async dbContext => postCaptured.Caption.Should().Be(post.Caption); postCaptured.Url.Should().Be(postCaptured.Url); } + + [Fact] + public async Task Returns_related_resources_on_broken_resource_linkage() + { + // Arrange + WebAccount account = _fakers.WebAccount.Generate(); + account.Posts = _fakers.BlogPost.Generate(2); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Accounts.Add(account); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/webAccounts/{account.StringId}?include=posts&fields[webAccounts]=displayName"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(account.StringId); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); + + responseDocument.Included.Should().HaveCount(2); + responseDocument.Included.Should().OnlyContain(resourceObject => resourceObject.Type == "blogPosts"); + } } } From 0f41f855aaf7b1684ae15bc3125b92e7c7320913 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 26 Oct 2021 09:56:00 +0200 Subject: [PATCH 48/49] Ported existing unit tests and changed how included[] is built. It now always emits related resources in relationship declaration order, even in deeply nested circular chains where a subsequent inclusion chain is deeper and adds more relationships to an already converted resource. Performance impact, summary: - In the endpoint test, this commit improves performance for rendering includes, while slightly decreasing it for the other two scenarios. All are still faster or the same compared to the master branch. - In BenchmarkDotNet, this commit slightly increases rendering time, compared to earlier commits in this PR, but it is still faster than the master branch. Measurement results for GET http://localhost:14140/api/v1/todoItems?include=owner,assignee,tags: Write response body ............................. 0:00:00:00.0010385 -> 0:00:00:00.0008060 ... 77% (was: 130%) Measurement results for GET http://localhost:14140/api/v1/todoItems?filter=and(startsWith(description,'T'),equals(priority,'Low'),not(equals(owner,null)),not(equals(assignee,null))): Write response body ............................. 0:00:00:00.0006601 -> 0:00:00:00.0005629 ... 85% (was: 70%) Measurement results for POST http://localhost:14140/api/v1/operations (10x add-resource): Write response body ............................. 0:00:00:00.0003432 -> 0:00:00:00.0003411 ... 99% (was: 95%) | Method | Mean | Error | StdDev | |---------------------------------- |---------:|--------:|--------:| | LegacySerializeOperationsResponse | 239.0 us | 3.17 us | 2.81 us | | SerializeOperationsResponse | 153.8 us | 0.72 us | 0.60 us | | (new) SerializeOperationsResponse | 168.6 us | 1.74 us | 1.63 us | | Method | Mean | Error | StdDev | |-------------------------------- |---------:|--------:|--------:| | LegacySerializeResourceResponse | 177.6 us | 0.56 us | 0.50 us | | SerializeResourceResponse | 101.3 us | 0.31 us | 0.29 us | | (new) SerializeResourceResponse | 123.7 us | 1.12 us | 1.05 us | --- .../Response/IncludedCollection.cs | 78 -- .../Response/ResourceObjectTreeNode.cs | 273 +++++++ .../Response/ResponseModelAdapter.cs | 253 +++--- .../Serialization/Response/Models/Article.cs | 19 + .../Serialization/Response/Models/Blog.cs | 19 + .../Serialization/Response/Models/Food.cs | 13 + .../Serialization/Response/Models/Person.cs | 23 + .../Serialization/Response/Models/Song.cs | 13 + .../Response/ResponseModelAdapterTests.cs | 718 ++++++++++++++++++ .../Response/ResponseSerializationFakers.cs | 49 ++ 10 files changed, 1271 insertions(+), 187 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Serialization/Response/IncludedCollection.cs create mode 100644 src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs create mode 100644 test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Article.cs create mode 100644 test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Blog.cs create mode 100644 test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Food.cs create mode 100644 test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Person.cs create mode 100644 test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Song.cs create mode 100644 test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs create mode 100644 test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseSerializationFakers.cs diff --git a/src/JsonApiDotNetCore/Serialization/Response/IncludedCollection.cs b/src/JsonApiDotNetCore/Serialization/Response/IncludedCollection.cs deleted file mode 100644 index 46f30c256b..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Response/IncludedCollection.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization.Response -{ - [PublicAPI] - public sealed class IncludedCollection - { - private readonly List _includes = new(); - private readonly Dictionary _resourceToIncludeIndexMap = new(IdentifiableComparer.Instance); - - public IList ResourceObjects => _includes; - - public ResourceObject AddOrUpdate(IIdentifiable resource, ResourceObject resourceObject) - { - if (!_resourceToIncludeIndexMap.ContainsKey(resource)) - { - _includes.Add(resourceObject); - _resourceToIncludeIndexMap.Add(resource, _includes.Count - 1); - } - else - { - if (resourceObject.Type != null) - { - int existingIndex = _resourceToIncludeIndexMap[resource]; - ResourceObject existingVersion = _includes[existingIndex]; - - if (existingVersion != resourceObject) - { - MergeRelationships(resourceObject, existingVersion); - - return existingVersion; - } - } - } - - return resourceObject; - } - - private static void MergeRelationships(ResourceObject incomingVersion, ResourceObject existingVersion) - { - // The code below handles the case where one resource is added through different include chains with different relationships. - // We enrich the existing resource object with the added relationships coming from the second chain, to ensure correct resource linkage. - // - // This is best explained using an example. Consider the next inclusion chains: - // - // 1. reviewer.loginAttempts - // 2. author.preferences - // - // Where the relationships `reviewer` and `author` are of the same resource type `people`. Then the next rules apply: - // - // A. People that were included as reviewers from inclusion chain (1) should come with their `loginAttempts` included, but not those from chain (2). - // B. People that were included as authors from inclusion chain (2) should come with their `preferences` included, but not those from chain (1). - // C. For a person that was included as both an reviewer and author (i.e. targeted by both chains), both `loginAttempts` and `preferences` need - // to be present. - // - // For rule (C), the related resources will be included as usual, but we need to fix resource linkage here by merging the relationship objects. - // - // Note that this implementation breaks the overall depth-first ordering of included objects. So solve that, we'd need to use a dependency graph - // for included objects instead of a flat list, which may affect performance. Since the ordering is not guaranteed anyway, keeping it simple for now. - - foreach ((string relationshipName, RelationshipObject relationshipObject) in existingVersion.Relationships.EmptyIfNull()) - { - if (!relationshipObject.Data.IsAssigned) - { - SingleOrManyData incomingRelationshipData = incomingVersion.Relationships[relationshipName].Data; - - if (incomingRelationshipData.IsAssigned) - { - relationshipObject.Data = incomingRelationshipData; - } - } - } - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs b/src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs new file mode 100644 index 0000000000..3e46f43571 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.Response +{ + /// + /// Represents a dependency tree of resource objects. It provides the values for 'data' and 'included' in the response body. The tree is built by + /// recursively walking the resource relationships from the inclusion chains. Note that a subsequent chain may add additional relationships to a resource + /// object that was produced by an earlier chain. Afterwards, this tree is used to fill relationship objects in the resource objects (depending on sparse + /// fieldsets) and to emit all entries in relationship declaration order. + /// + internal sealed class ResourceObjectTreeNode : IEquatable + { + // Placeholder root node for the tree, which is never emitted itself. + private static readonly ResourceType RootType = new("(root)", typeof(object), typeof(object)); + private static readonly IIdentifiable RootResource = new EmptyResource(); + + // Direct children from root. These are emitted in 'data'. + private List _directChildren; + + // Related resource objects per relationship. These are emitted in 'included'. + private Dictionary> _childrenByRelationship; + + private bool IsTreeRoot => RootType.Equals(Type); + + // The resource this node was built for. We only store it for the LinkBuilder. + public IIdentifiable Resource { get; } + + // The resource type. We use its relationships to maintain order. + public ResourceType Type { get; } + + // The produced resource object from Resource. For each resource, at most one ResourceObject and one tree node must exist. + public ResourceObject ResourceObject { get; } + + public ResourceObjectTreeNode(IIdentifiable resource, ResourceType type, ResourceObject resourceObject) + { + ArgumentGuard.NotNull(resource, nameof(resource)); + ArgumentGuard.NotNull(type, nameof(type)); + ArgumentGuard.NotNull(resourceObject, nameof(resourceObject)); + + Resource = resource; + Type = type; + ResourceObject = resourceObject; + } + + public static ResourceObjectTreeNode CreateRoot() + { + return new ResourceObjectTreeNode(RootResource, RootType, new ResourceObject()); + } + + public void AttachDirectChild(ResourceObjectTreeNode treeNode) + { + ArgumentGuard.NotNull(treeNode, nameof(treeNode)); + + _directChildren ??= new List(); + _directChildren.Add(treeNode); + } + + public void EnsureHasRelationship(RelationshipAttribute relationship) + { + ArgumentGuard.NotNull(relationship, nameof(relationship)); + + _childrenByRelationship ??= new Dictionary>(); + + if (!_childrenByRelationship.ContainsKey(relationship)) + { + _childrenByRelationship[relationship] = new HashSet(); + } + } + + public void AttachRelationshipChild(RelationshipAttribute relationship, ResourceObjectTreeNode rightNode) + { + ArgumentGuard.NotNull(relationship, nameof(relationship)); + ArgumentGuard.NotNull(rightNode, nameof(rightNode)); + + HashSet rightNodes = _childrenByRelationship[relationship]; + rightNodes.Add(rightNode); + } + + /// + /// Recursively walks the tree and returns the set of unique nodes. Uses relationship declaration order. + /// + public ISet GetUniqueNodes() + { + AssertIsTreeRoot(); + + var visited = new HashSet(); + + VisitSubtree(this, visited); + + return visited; + } + + private static void VisitSubtree(ResourceObjectTreeNode treeNode, ISet visited) + { + if (visited.Contains(treeNode)) + { + return; + } + + if (!treeNode.IsTreeRoot) + { + visited.Add(treeNode); + } + + VisitDirectChildrenInSubtree(treeNode, visited); + VisitRelationshipChildrenInSubtree(treeNode, visited); + } + + private static void VisitDirectChildrenInSubtree(ResourceObjectTreeNode treeNode, ISet visited) + { + if (treeNode._directChildren != null) + { + foreach (ResourceObjectTreeNode child in treeNode._directChildren) + { + VisitSubtree(child, visited); + } + } + } + + private static void VisitRelationshipChildrenInSubtree(ResourceObjectTreeNode treeNode, ISet visited) + { + if (treeNode._childrenByRelationship != null) + { + foreach (RelationshipAttribute relationship in treeNode.Type.Relationships) + { + if (treeNode._childrenByRelationship.TryGetValue(relationship, out HashSet rightNodes)) + { + VisitRelationshipChildInSubtree(rightNodes, visited); + } + } + } + } + + private static void VisitRelationshipChildInSubtree(HashSet rightNodes, ISet visited) + { + foreach (ResourceObjectTreeNode rightNode in rightNodes) + { + VisitSubtree(rightNode, visited); + } + } + + public ISet GetRightNodesInRelationship(RelationshipAttribute relationship) + { + return _childrenByRelationship != null && _childrenByRelationship.TryGetValue(relationship, out HashSet rightNodes) + ? rightNodes + : null; + } + + /// + /// Provides the value for 'data' in the response body. Uses relationship declaration order. + /// + public IList GetResponseData() + { + AssertIsTreeRoot(); + + return GetDirectChildren().Select(child => child.ResourceObject).ToArray(); + } + + /// + /// Provides the value for 'included' in the response body. Uses relationship declaration order. + /// + public IList GetResponseIncluded() + { + AssertIsTreeRoot(); + + var visited = new HashSet(); + + foreach (ResourceObjectTreeNode child in GetDirectChildren()) + { + VisitRelationshipChildrenInSubtree(child, visited); + } + + return visited.Select(node => node.ResourceObject).ToArray(); + } + + private IList GetDirectChildren() + { + return _directChildren == null ? Array.Empty() : _directChildren; + } + + private void AssertIsTreeRoot() + { + if (!IsTreeRoot) + { + throw new InvalidOperationException("Internal error: this method should only be called from the root of the tree."); + } + } + + public bool Equals(ResourceObjectTreeNode other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return ResourceObjectComparer.Instance.Equals(ResourceObject, other.ResourceObject); + } + + public override bool Equals(object other) + { + return Equals(other as ResourceObjectTreeNode); + } + + public override int GetHashCode() + { + return ResourceObject.GetHashCode(); + } + + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append(IsTreeRoot ? Type.PublicName : $"{ResourceObject.Type}:{ResourceObject.Id}"); + + if (_directChildren != null) + { + builder.Append($", children: {_directChildren.Count}"); + } + else if (_childrenByRelationship != null) + { + builder.Append($", children: {string.Join(',', _childrenByRelationship.Select(pair => $"{pair.Key.PublicName} ({pair.Value.Count})"))}"); + } + + return builder.ToString(); + } + + private sealed class EmptyResource : IIdentifiable + { + public string StringId { get; set; } + public string LocalId { get; set; } + } + + private sealed class ResourceObjectComparer : IEqualityComparer + { + public static readonly ResourceObjectComparer Instance = new(); + + private ResourceObjectComparer() + { + } + + public bool Equals(ResourceObject x, ResourceObject y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null || y is null || x.GetType() != y.GetType()) + { + return false; + } + + return x.Type == y.Type && x.Id == y.Id && x.Lid == y.Lid; + } + + public int GetHashCode(ResourceObject obj) + { + return HashCode.Combine(obj.Type, obj.Id, obj.Lid); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs index f61a6e7aa5..10f25f2c20 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs @@ -32,6 +32,9 @@ public class ResponseModelAdapter : IResponseModelAdapter private readonly IRequestQueryStringAccessor _requestQueryStringAccessor; private readonly ISparseFieldSetCache _sparseFieldSetCache; + // Ensures that at most one ResourceObject (and one tree node) is produced per resource instance. + private readonly Dictionary _resourceToTreeNodeCache = new(IdentifiableComparer.Instance); + public ResponseModelAdapter(IJsonApiRequest request, IJsonApiOptions options, ILinkBuilder linkBuilder, IMetaBuilder metaBuilder, IResourceDefinitionAccessor resourceDefinitionAccessor, IEvaluatedIncludeCache evaluatedIncludeCache, ISparseFieldSetCache sparseFieldSetCache, IRequestQueryStringAccessor requestQueryStringAccessor) @@ -59,25 +62,34 @@ public ResponseModelAdapter(IJsonApiRequest request, IJsonApiOptions options, IL public Document Convert(object model) { _sparseFieldSetCache.Reset(); + _resourceToTreeNodeCache.Clear(); var document = new Document(); IncludeExpression include = _evaluatedIncludeCache.Get(); IImmutableSet includeElements = include?.Elements ?? ImmutableHashSet.Empty; - var includedCollection = new IncludedCollection(); + var rootNode = ResourceObjectTreeNode.CreateRoot(); ResourceType resourceType = _request.SecondaryResourceType ?? _request.PrimaryResourceType; if (model is IEnumerable resources) { - IEnumerable resourceObjects = resources.Select(resource => - ConvertResource(resource, resourceType, _request.Kind, includeElements, includedCollection, false)); + foreach (IIdentifiable resource in resources) + { + TraverseResource(resource, resourceType, _request.Kind, includeElements, rootNode, null); + } + PopulateRelationshipsInTree(rootNode, _request.Kind); + + IEnumerable resourceObjects = rootNode.GetResponseData(); document.Data = new SingleOrManyData(resourceObjects); } else if (model is IIdentifiable resource) { - ResourceObject resourceObject = ConvertResource(resource, resourceType, _request.Kind, includeElements, includedCollection, false); + TraverseResource(resource, resourceType, _request.Kind, includeElements, rootNode, null); + PopulateRelationshipsInTree(rootNode, _request.Kind); + + ResourceObject resourceObject = rootNode.GetResponseData().Single(); document.Data = new SingleOrManyData(resourceObject); } else if (model == null) @@ -87,7 +99,7 @@ public Document Convert(object model) else if (model is IEnumerable operations) { using var _ = new RevertRequestStateOnDispose(_request, null); - document.Results = operations.Select(operation => ConvertOperation(operation, includeElements, includedCollection)).ToList(); + document.Results = operations.Select(operation => ConvertOperation(operation, includeElements)).ToList(); } else if (model is IEnumerable errorObjects) { @@ -105,13 +117,12 @@ public Document Convert(object model) document.JsonApi = GetApiObject(); document.Links = _linkBuilder.GetTopLevelLinks(); document.Meta = _metaBuilder.Build(); - document.Included = GetIncluded(includedCollection); + document.Included = GetIncluded(rootNode); return document; } - protected virtual AtomicResultObject ConvertOperation(OperationContainer operation, IImmutableSet includeElements, - IncludedCollection includedCollection) + protected virtual AtomicResultObject ConvertOperation(OperationContainer operation, IImmutableSet includeElements) { ResourceObject resourceObject = null; @@ -120,9 +131,15 @@ protected virtual AtomicResultObject ConvertOperation(OperationContainer operati _request.CopyFrom(operation.Request); ResourceType resourceType = operation.Request.SecondaryResourceType ?? operation.Request.PrimaryResourceType; - resourceObject = ConvertResource(operation.Resource, resourceType, operation.Request.Kind, includeElements, includedCollection, false); + var rootNode = ResourceObjectTreeNode.CreateRoot(); + + TraverseResource(operation.Resource, resourceType, operation.Request.Kind, includeElements, rootNode, null); + PopulateRelationshipsInTree(rootNode, operation.Request.Kind); + + resourceObject = rootNode.GetResponseData().Single(); _sparseFieldSetCache.Reset(); + _resourceToTreeNodeCache.Clear(); } return new AtomicResultObject @@ -131,33 +148,62 @@ protected virtual AtomicResultObject ConvertOperation(OperationContainer operati }; } - protected virtual ResourceObject ConvertResource(IIdentifiable resource, ResourceType resourceType, EndpointKind requestKind, - IImmutableSet includeElements, IncludedCollection includedCollection, bool isInclude) + private void TraverseResource(IIdentifiable resource, ResourceType type, EndpointKind kind, IImmutableSet includeElements, + ResourceObjectTreeNode parentTreeNode, RelationshipAttribute parentRelationship) { - IImmutableSet fieldSet = null; + ResourceObjectTreeNode treeNode = GetOrCreateTreeNode(resource, type, kind); - if (requestKind != EndpointKind.Relationship) + if (parentRelationship != null) { - _resourceDefinitionAccessor.OnSerialize(resource); + parentTreeNode.AttachRelationshipChild(parentRelationship, treeNode); + } + else + { + parentTreeNode.AttachDirectChild(treeNode); + } - fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceType); + if (kind != EndpointKind.Relationship) + { + TraverseRelationships(resource, treeNode, includeElements, kind); } + } - var resourceObject = new ResourceObject(); + private ResourceObjectTreeNode GetOrCreateTreeNode(IIdentifiable resource, ResourceType type, EndpointKind kind) + { + if (!_resourceToTreeNodeCache.TryGetValue(resource, out ResourceObjectTreeNode treeNode)) + { + ResourceObject resourceObject = ConvertResource(resource, type, kind); + treeNode = new ResourceObjectTreeNode(resource, type, resourceObject); - if (isInclude) + _resourceToTreeNodeCache.Add(resource, treeNode); + } + + return treeNode; + } + + protected virtual ResourceObject ConvertResource(IIdentifiable resource, ResourceType type, EndpointKind kind) + { + bool isRelationship = kind == EndpointKind.Relationship; + + if (!isRelationship) { - resourceObject = includedCollection.AddOrUpdate(resource, resourceObject); + _resourceDefinitionAccessor.OnSerialize(resource); } - bool isRelationship = requestKind == EndpointKind.Relationship; + var resourceObject = new ResourceObject + { + Type = type.PublicName, + Id = resource.StringId + }; + + if (!isRelationship) + { + IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(type); - resourceObject.Type = resourceType.PublicName; - resourceObject.Id = resource.StringId; - resourceObject.Attributes = ConvertAttributes(resource, resourceType, fieldSet); - resourceObject.Relationships = ConvertRelationships(resource, resourceType, fieldSet, requestKind, includeElements, includedCollection); - resourceObject.Links = isRelationship ? null : _linkBuilder.GetResourceLinks(resourceType, resource.StringId); - resourceObject.Meta = isRelationship ? null : _resourceDefinitionAccessor.GetMeta(resourceType, resource); + resourceObject.Attributes = ConvertAttributes(resource, type, fieldSet); + resourceObject.Links = _linkBuilder.GetResourceLinks(type, resource.StringId); + resourceObject.Meta = _resourceDefinitionAccessor.GetMeta(type, resource); + } return resourceObject; } @@ -165,130 +211,117 @@ protected virtual ResourceObject ConvertResource(IIdentifiable resource, Resourc protected virtual IDictionary ConvertAttributes(IIdentifiable resource, ResourceType resourceType, IImmutableSet fieldSet) { - if (fieldSet != null) - { - var attrMap = new Dictionary(resourceType.Attributes.Count); + var attrMap = new Dictionary(resourceType.Attributes.Count); - foreach (AttrAttribute attr in resourceType.Attributes) + foreach (AttrAttribute attr in resourceType.Attributes) + { + if (!fieldSet.Contains(attr) || attr.Property.Name == nameof(Identifiable.Id)) { - if (!fieldSet.Contains(attr) || attr.Property.Name == nameof(Identifiable.Id)) - { - continue; - } - - object value = attr.GetValue(resource); - - if (_options.SerializerOptions.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull && value == null) - { - continue; - } + continue; + } - if (_options.SerializerOptions.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingDefault && - Equals(value, RuntimeTypeConverter.GetDefaultValue(attr.Property.PropertyType))) - { - continue; - } + object value = attr.GetValue(resource); - attrMap.Add(attr.PublicName, value); + if (_options.SerializerOptions.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull && value == null) + { + continue; } - if (attrMap.Any()) + if (_options.SerializerOptions.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingDefault && + Equals(value, RuntimeTypeConverter.GetDefaultValue(attr.Property.PropertyType))) { - return attrMap; + continue; } + + attrMap.Add(attr.PublicName, value); } - return null; + return attrMap.Any() ? attrMap : null; } - protected virtual IDictionary ConvertRelationships(IIdentifiable resource, ResourceType resourceType, - IImmutableSet fieldSet, EndpointKind requestKind, IImmutableSet includeElements, - IncludedCollection includedCollection) + private void TraverseRelationships(IIdentifiable leftResource, ResourceObjectTreeNode leftTreeNode, + IImmutableSet includeElements, EndpointKind kind) { - if (fieldSet != null) + foreach (IncludeElementExpression includeElement in includeElements) { - var relationshipMap = new Dictionary(resourceType.Relationships.Count); + TraverseRelationship(includeElement.Relationship, leftResource, leftTreeNode, includeElement, kind); + } + } - foreach (RelationshipAttribute relationship in resourceType.Relationships) - { - IncludeElementExpression includeElement = GetFirstOrDefault(includeElements, relationship, - (element, nextRelationship) => element.Relationship.Equals(nextRelationship)); + private void TraverseRelationship(RelationshipAttribute relationship, IIdentifiable leftResource, ResourceObjectTreeNode leftTreeNode, + IncludeElementExpression includeElement, EndpointKind kind) + { + object rightValue = relationship.GetValue(leftResource); + ICollection rightResources = CollectionConverter.ExtractResources(rightValue); - RelationshipObject relationshipObject = ConvertRelationship(relationship, resource, requestKind, includeElement, includedCollection); + leftTreeNode.EnsureHasRelationship(relationship); - if (relationshipObject != null && fieldSet.Contains(relationship)) - { - relationshipMap.Add(relationship.PublicName, relationshipObject); - } - } + foreach (IIdentifiable rightResource in rightResources) + { + TraverseResource(rightResource, relationship.RightType, kind, includeElement.Children, leftTreeNode, relationship); + } + } - if (relationshipMap.Any()) + private void PopulateRelationshipsInTree(ResourceObjectTreeNode rootNode, EndpointKind kind) + { + if (kind != EndpointKind.Relationship) + { + foreach (ResourceObjectTreeNode treeNode in rootNode.GetUniqueNodes()) { - return relationshipMap; + PopulateRelationshipsInResourceObject(treeNode); } } - - return null; } - private static TSource GetFirstOrDefault(IEnumerable source, TContext context, Func condition) + private void PopulateRelationshipsInResourceObject(ResourceObjectTreeNode treeNode) { - // PERF: This replacement for Enumerable.FirstOrDefault() doesn't allocate a compiler-generated closure class <>c__DisplayClass. - // https://www.jetbrains.com/help/resharper/2021.2/Fixing_Issues_Found_by_DPA.html#closures-in-lambda-expressions + IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(treeNode.Type); - foreach (TSource item in source) + foreach (RelationshipAttribute relationship in treeNode.Type.Relationships) { - if (condition(item, context)) + if (fieldSet.Contains(relationship)) { - return item; + PopulateRelationshipInResourceObject(treeNode, relationship); } } - - return default; } - protected virtual RelationshipObject ConvertRelationship(RelationshipAttribute relationship, IIdentifiable leftResource, EndpointKind requestKind, - IncludeElementExpression includeElement, IncludedCollection includedCollection) + private void PopulateRelationshipInResourceObject(ResourceObjectTreeNode treeNode, RelationshipAttribute relationship) { - SingleOrManyData data = default; + SingleOrManyData data = GetRelationshipData(treeNode, relationship); + RelationshipLinks links = _linkBuilder.GetRelationshipLinks(relationship, treeNode.Resource); - if (includeElement != null) + if (links != null || data.IsAssigned) { - object rightValue = relationship.GetValue(leftResource); - ICollection rightResources = CollectionConverter.ExtractResources(rightValue); - - var resourceIdentifierObjects = new List(rightResources.Count); - - foreach (IIdentifiable rightResource in rightResources) + var relationshipObject = new RelationshipObject { - var resourceIdentifierObject = new ResourceIdentifierObject - { - Type = relationship.RightType.PublicName, - Id = rightResource.StringId - }; + Links = links, + Data = data + }; - resourceIdentifierObjects.Add(resourceIdentifierObject); + treeNode.ResourceObject.Relationships ??= new Dictionary(); + treeNode.ResourceObject.Relationships.Add(relationship.PublicName, relationshipObject); + } + } - ResourceObject includeResource = ConvertResource(rightResource, relationship.RightType, requestKind, includeElement.Children, - includedCollection, true); + private static SingleOrManyData GetRelationshipData(ResourceObjectTreeNode treeNode, RelationshipAttribute relationship) + { + ISet rightNodes = treeNode.GetRightNodesInRelationship(relationship); - includedCollection.AddOrUpdate(rightResource, includeResource); - } + if (rightNodes != null) + { + IEnumerable resourceIdentifierObjects = rightNodes.Select(rightNode => new ResourceIdentifierObject + { + Type = rightNode.Type.PublicName, + Id = rightNode.ResourceObject.Id + }); - data = relationship is HasOneAttribute + return relationship is HasOneAttribute ? new SingleOrManyData(resourceIdentifierObjects.SingleOrDefault()) : new SingleOrManyData(resourceIdentifierObjects); } - RelationshipLinks links = _linkBuilder.GetRelationshipLinks(relationship, leftResource); - - return links == null && !data.IsAssigned - ? null - : new RelationshipObject - { - Links = links, - Data = data - }; + return default; } protected virtual JsonApiObject GetApiObject() @@ -314,11 +347,13 @@ protected virtual JsonApiObject GetApiObject() return jsonApiObject; } - protected virtual IList GetIncluded(IncludedCollection includedCollection) + private IList GetIncluded(ResourceObjectTreeNode rootNode) { - if (includedCollection.ResourceObjects.Any()) + IList resourceObjects = rootNode.GetResponseIncluded(); + + if (resourceObjects.Any()) { - return includedCollection.ResourceObjects; + return resourceObjects; } return _requestQueryStringAccessor.Query.ContainsKey("include") ? Array.Empty() : null; diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Article.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Article.cs new file mode 100644 index 0000000000..ce79fe44f7 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Article.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreTests.UnitTests.Serialization.Response.Models +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Article : Identifiable + { + [Attr] + public string Title { get; set; } + + [HasOne] + public Person Reviewer { get; set; } + + [HasOne] + public Person Author { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Blog.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Blog.cs new file mode 100644 index 0000000000..8438fc2c0a --- /dev/null +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Blog.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreTests.UnitTests.Serialization.Response.Models +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Blog : Identifiable + { + [Attr] + public string Title { get; set; } + + [HasOne] + public Person Reviewer { get; set; } + + [HasOne] + public Person Author { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Food.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Food.cs new file mode 100644 index 0000000000..5acc9d6bb8 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Food.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreTests.UnitTests.Serialization.Response.Models +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Food : Identifiable + { + [Attr] + public string Dish { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Person.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Person.cs new file mode 100644 index 0000000000..0c5972b291 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Person.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreTests.UnitTests.Serialization.Response.Models +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Person : Identifiable + { + [Attr] + public string Name { get; set; } + + [HasMany] + public ISet Blogs { get; set; } + + [HasOne] + public Food FavoriteFood { get; set; } + + [HasOne] + public Song FavoriteSong { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Song.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Song.cs new file mode 100644 index 0000000000..6bda0ab81b --- /dev/null +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/Models/Song.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreTests.UnitTests.Serialization.Response.Models +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Song : Identifiable + { + [Attr] + public string Title { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs new file mode 100644 index 0000000000..c19a40fc79 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs @@ -0,0 +1,718 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Queries.Internal; +using JsonApiDotNetCore.Queries.Internal.Parsing; +using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.Serialization.Response; +using JsonApiDotNetCoreTests.UnitTests.Serialization.Response.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Abstractions; +using TestBuildingBlocks; +using Xunit; + +namespace JsonApiDotNetCoreTests.UnitTests.Serialization.Response +{ + public sealed class ResponseModelAdapterTests + { + [Fact] + public void Resources_in_deeply_nested_circular_chain_are_written_in_relationship_declaration_order() + { + // Arrange + var fakers = new ResponseSerializationFakers(); + + Article article = fakers.Article.Generate(); + article.Author = fakers.Person.Generate(); + article.Author.Blogs = fakers.Blog.Generate(2).ToHashSet(); + article.Author.Blogs.ElementAt(0).Reviewer = article.Author; + article.Author.Blogs.ElementAt(1).Reviewer = fakers.Person.Generate(); + article.Author.FavoriteFood = fakers.Food.Generate(); + article.Author.Blogs.ElementAt(1).Reviewer.FavoriteFood = fakers.Food.Generate(); + + IJsonApiOptions options = new JsonApiOptions(); + ResponseModelAdapter responseModelAdapter = CreateAdapter(options, article.StringId, "author.blogs.reviewer.favoriteFood"); + + // Act + Document document = responseModelAdapter.Convert(article); + + // Assert + string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); + + text.Should().BeJson(@"{ + ""data"": { + ""type"": ""articles"", + ""id"": ""1"", + ""attributes"": { + ""title"": ""The SAS microchip is down, quantify the 1080p microchip so we can quantify the SAS microchip!"" + }, + ""relationships"": { + ""author"": { + ""data"": { + ""type"": ""people"", + ""id"": ""2"" + } + } + } + }, + ""included"": [ + { + ""type"": ""people"", + ""id"": ""2"", + ""attributes"": { + ""name"": ""Ernestine Runte"" + }, + ""relationships"": { + ""blogs"": { + ""data"": [ + { + ""type"": ""blogs"", + ""id"": ""3"" + }, + { + ""type"": ""blogs"", + ""id"": ""4"" + } + ] + }, + ""favoriteFood"": { + ""data"": { + ""type"": ""foods"", + ""id"": ""6"" + } + } + } + }, + { + ""type"": ""blogs"", + ""id"": ""3"", + ""attributes"": { + ""title"": ""The SAS microchip is down, quantify the 1080p microchip so we can quantify the SAS microchip!"" + }, + ""relationships"": { + ""reviewer"": { + ""data"": { + ""type"": ""people"", + ""id"": ""2"" + } + } + } + }, + { + ""type"": ""blogs"", + ""id"": ""4"", + ""attributes"": { + ""title"": ""I'll connect the mobile ADP card, that should card the ADP card!"" + }, + ""relationships"": { + ""reviewer"": { + ""data"": { + ""type"": ""people"", + ""id"": ""5"" + } + } + } + }, + { + ""type"": ""people"", + ""id"": ""5"", + ""attributes"": { + ""name"": ""Doug Shields"" + }, + ""relationships"": { + ""favoriteFood"": { + ""data"": { + ""type"": ""foods"", + ""id"": ""7"" + } + } + } + }, + { + ""type"": ""foods"", + ""id"": ""7"", + ""attributes"": { + ""dish"": ""Nostrum totam harum totam voluptatibus."" + } + }, + { + ""type"": ""foods"", + ""id"": ""6"", + ""attributes"": { + ""dish"": ""Illum assumenda iste quia natus et dignissimos reiciendis."" + } + } + ] +}"); + } + + [Fact] + public void Resources_in_deeply_nested_circular_chains_are_written_in_relationship_declaration_order_without_duplicates() + { + // Arrange + var fakers = new ResponseSerializationFakers(); + + Article article1 = fakers.Article.Generate(); + article1.Author = fakers.Person.Generate(); + article1.Author.Blogs = fakers.Blog.Generate(2).ToHashSet(); + article1.Author.Blogs.ElementAt(0).Reviewer = article1.Author; + article1.Author.Blogs.ElementAt(1).Reviewer = fakers.Person.Generate(); + article1.Author.FavoriteFood = fakers.Food.Generate(); + article1.Author.Blogs.ElementAt(1).Reviewer.FavoriteFood = fakers.Food.Generate(); + + Article article2 = fakers.Article.Generate(); + article2.Author = article1.Author; + + IJsonApiOptions options = new JsonApiOptions(); + ResponseModelAdapter responseModelAdapter = CreateAdapter(options, article1.StringId, "author.blogs.reviewer.favoriteFood"); + + // Act + Document document = responseModelAdapter.Convert(new[] + { + article1, + article2 + }); + + // Assert + string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); + + text.Should().BeJson(@"{ + ""data"": [ + { + ""type"": ""articles"", + ""id"": ""1"", + ""attributes"": { + ""title"": ""The SAS microchip is down, quantify the 1080p microchip so we can quantify the SAS microchip!"" + }, + ""relationships"": { + ""author"": { + ""data"": { + ""type"": ""people"", + ""id"": ""2"" + } + } + } + }, + { + ""type"": ""articles"", + ""id"": ""8"", + ""attributes"": { + ""title"": ""I'll connect the mobile ADP card, that should card the ADP card!"" + }, + ""relationships"": { + ""author"": { + ""data"": { + ""type"": ""people"", + ""id"": ""2"" + } + } + } + } + ], + ""included"": [ + { + ""type"": ""people"", + ""id"": ""2"", + ""attributes"": { + ""name"": ""Ernestine Runte"" + }, + ""relationships"": { + ""blogs"": { + ""data"": [ + { + ""type"": ""blogs"", + ""id"": ""3"" + }, + { + ""type"": ""blogs"", + ""id"": ""4"" + } + ] + }, + ""favoriteFood"": { + ""data"": { + ""type"": ""foods"", + ""id"": ""6"" + } + } + } + }, + { + ""type"": ""blogs"", + ""id"": ""3"", + ""attributes"": { + ""title"": ""The SAS microchip is down, quantify the 1080p microchip so we can quantify the SAS microchip!"" + }, + ""relationships"": { + ""reviewer"": { + ""data"": { + ""type"": ""people"", + ""id"": ""2"" + } + } + } + }, + { + ""type"": ""blogs"", + ""id"": ""4"", + ""attributes"": { + ""title"": ""I'll connect the mobile ADP card, that should card the ADP card!"" + }, + ""relationships"": { + ""reviewer"": { + ""data"": { + ""type"": ""people"", + ""id"": ""5"" + } + } + } + }, + { + ""type"": ""people"", + ""id"": ""5"", + ""attributes"": { + ""name"": ""Doug Shields"" + }, + ""relationships"": { + ""favoriteFood"": { + ""data"": { + ""type"": ""foods"", + ""id"": ""7"" + } + } + } + }, + { + ""type"": ""foods"", + ""id"": ""7"", + ""attributes"": { + ""dish"": ""Nostrum totam harum totam voluptatibus."" + } + }, + { + ""type"": ""foods"", + ""id"": ""6"", + ""attributes"": { + ""dish"": ""Illum assumenda iste quia natus et dignissimos reiciendis."" + } + } + ] +}"); + } + + [Fact] + public void Resources_in_overlapping_deeply_nested_circular_chains_are_written_in_relationship_declaration_order() + { + // Arrange + var fakers = new ResponseSerializationFakers(); + + Article article = fakers.Article.Generate(); + article.Author = fakers.Person.Generate(); + article.Author.Blogs = fakers.Blog.Generate(2).ToHashSet(); + article.Author.Blogs.ElementAt(0).Reviewer = article.Author; + article.Author.Blogs.ElementAt(1).Reviewer = fakers.Person.Generate(); + article.Author.FavoriteFood = fakers.Food.Generate(); + article.Author.Blogs.ElementAt(1).Reviewer.FavoriteFood = fakers.Food.Generate(); + + article.Reviewer = fakers.Person.Generate(); + article.Reviewer.Blogs = fakers.Blog.Generate(1).ToHashSet(); + article.Reviewer.Blogs.Add(article.Author.Blogs.ElementAt(0)); + article.Reviewer.Blogs.ElementAt(0).Author = article.Reviewer; + + article.Reviewer.Blogs.ElementAt(1).Author = article.Author.Blogs.ElementAt(1).Reviewer; + article.Author.Blogs.ElementAt(1).Reviewer.FavoriteSong = fakers.Song.Generate(); + article.Reviewer.FavoriteSong = fakers.Song.Generate(); + + IJsonApiOptions options = new JsonApiOptions(); + + ResponseModelAdapter responseModelAdapter = + CreateAdapter(options, article.StringId, "author.blogs.reviewer.favoriteFood,reviewer.blogs.author.favoriteSong"); + + // Act + Document document = responseModelAdapter.Convert(article); + + // Assert + string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); + + text.Should().BeJson(@"{ + ""data"": { + ""type"": ""articles"", + ""id"": ""1"", + ""attributes"": { + ""title"": ""The SAS microchip is down, quantify the 1080p microchip so we can quantify the SAS microchip!"" + }, + ""relationships"": { + ""reviewer"": { + ""data"": { + ""type"": ""people"", + ""id"": ""8"" + } + }, + ""author"": { + ""data"": { + ""type"": ""people"", + ""id"": ""2"" + } + } + } + }, + ""included"": [ + { + ""type"": ""people"", + ""id"": ""8"", + ""attributes"": { + ""name"": ""Nettie Howell"" + }, + ""relationships"": { + ""blogs"": { + ""data"": [ + { + ""type"": ""blogs"", + ""id"": ""9"" + }, + { + ""type"": ""blogs"", + ""id"": ""3"" + } + ] + }, + ""favoriteSong"": { + ""data"": { + ""type"": ""songs"", + ""id"": ""11"" + } + } + } + }, + { + ""type"": ""blogs"", + ""id"": ""9"", + ""attributes"": { + ""title"": ""The RSS bus is down, parse the mobile bus so we can parse the RSS bus!"" + }, + ""relationships"": { + ""author"": { + ""data"": { + ""type"": ""people"", + ""id"": ""8"" + } + } + } + }, + { + ""type"": ""blogs"", + ""id"": ""3"", + ""attributes"": { + ""title"": ""The SAS microchip is down, quantify the 1080p microchip so we can quantify the SAS microchip!"" + }, + ""relationships"": { + ""reviewer"": { + ""data"": { + ""type"": ""people"", + ""id"": ""2"" + } + }, + ""author"": { + ""data"": { + ""type"": ""people"", + ""id"": ""5"" + } + } + } + }, + { + ""type"": ""people"", + ""id"": ""2"", + ""attributes"": { + ""name"": ""Ernestine Runte"" + }, + ""relationships"": { + ""blogs"": { + ""data"": [ + { + ""type"": ""blogs"", + ""id"": ""3"" + }, + { + ""type"": ""blogs"", + ""id"": ""4"" + } + ] + }, + ""favoriteFood"": { + ""data"": { + ""type"": ""foods"", + ""id"": ""6"" + } + } + } + }, + { + ""type"": ""blogs"", + ""id"": ""4"", + ""attributes"": { + ""title"": ""I'll connect the mobile ADP card, that should card the ADP card!"" + }, + ""relationships"": { + ""reviewer"": { + ""data"": { + ""type"": ""people"", + ""id"": ""5"" + } + } + } + }, + { + ""type"": ""people"", + ""id"": ""5"", + ""attributes"": { + ""name"": ""Doug Shields"" + }, + ""relationships"": { + ""favoriteFood"": { + ""data"": { + ""type"": ""foods"", + ""id"": ""7"" + } + }, + ""favoriteSong"": { + ""data"": { + ""type"": ""songs"", + ""id"": ""10"" + } + } + } + }, + { + ""type"": ""foods"", + ""id"": ""7"", + ""attributes"": { + ""dish"": ""Nostrum totam harum totam voluptatibus."" + } + }, + { + ""type"": ""songs"", + ""id"": ""10"", + ""attributes"": { + ""title"": ""Illum assumenda iste quia natus et dignissimos reiciendis."" + } + }, + { + ""type"": ""foods"", + ""id"": ""6"", + ""attributes"": { + ""dish"": ""Illum assumenda iste quia natus et dignissimos reiciendis."" + } + }, + { + ""type"": ""songs"", + ""id"": ""11"", + ""attributes"": { + ""title"": ""Nostrum totam harum totam voluptatibus."" + } + } + ] +}"); + } + + [Fact] + public void Duplicate_children_in_multiple_chains_occur_once_in_output() + { + // Arrange + var fakers = new ResponseSerializationFakers(); + + Person person = fakers.Person.Generate(); + List
articles = fakers.Article.Generate(5); + articles.ForEach(article => article.Author = person); + articles.ForEach(article => article.Reviewer = person); + + IJsonApiOptions options = new JsonApiOptions(); + ResponseModelAdapter responseModelAdapter = CreateAdapter(options, null, "author,reviewer"); + + // Act + Document document = responseModelAdapter.Convert(articles); + + // Assert + document.Included.Should().HaveCount(1); + + document.Included[0].Attributes["name"].Should().Be(person.Name); + document.Included[0].Id.Should().Be(person.StringId); + } + + private ResponseModelAdapter CreateAdapter(IJsonApiOptions options, string primaryId, string includeChains) + { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + + IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance) + .Add
() + .Add() + .Add() + .Add() + .Add() + .Build(); + + // @formatter:wrap_chained_method_calls restore + // @formatter:keep_existing_linebreaks restore + + var request = new JsonApiRequest + { + Kind = EndpointKind.Primary, + PrimaryResourceType = resourceGraph.GetResourceType
(), + PrimaryId = primaryId + }; + + var evaluatedIncludeCache = new EvaluatedIncludeCache(); + var linkBuilder = new FakeLinkBuilder(); + var metaBuilder = new FakeMetaBuilder(); + var resourceDefinitionAccessor = new FakeResourceDefinitionAccessor(); + var sparseFieldSetCache = new SparseFieldSetCache(Array.Empty(), resourceDefinitionAccessor); + var requestQueryStringAccessor = new FakeRequestQueryStringAccessor(); + + var parser = new IncludeParser(); + IncludeExpression include = parser.Parse(includeChains, request.PrimaryResourceType, null); + evaluatedIncludeCache.Set(include); + + return new ResponseModelAdapter(request, options, linkBuilder, metaBuilder, resourceDefinitionAccessor, evaluatedIncludeCache, sparseFieldSetCache, + requestQueryStringAccessor); + } + + private sealed class FakeLinkBuilder : ILinkBuilder + { + public TopLevelLinks GetTopLevelLinks() + { + return null; + } + + public ResourceLinks GetResourceLinks(ResourceType resourceType, string id) + { + return null; + } + + public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable leftResource) + { + return null; + } + } + + private sealed class FakeMetaBuilder : IMetaBuilder + { + public void Add(IReadOnlyDictionary values) + { + } + + public IDictionary Build() + { + return null; + } + } + + private sealed class FakeResourceDefinitionAccessor : IResourceDefinitionAccessor + { + public IImmutableSet OnApplyIncludes(ResourceType resourceType, IImmutableSet existingIncludes) + { + return existingIncludes; + } + + public FilterExpression OnApplyFilter(ResourceType resourceType, FilterExpression existingFilter) + { + return existingFilter; + } + + public SortExpression OnApplySort(ResourceType resourceType, SortExpression existingSort) + { + return existingSort; + } + + public PaginationExpression OnApplyPagination(ResourceType resourceType, PaginationExpression existingPagination) + { + return existingPagination; + } + + public SparseFieldSetExpression OnApplySparseFieldSet(ResourceType resourceType, SparseFieldSetExpression existingSparseFieldSet) + { + return existingSparseFieldSet; + } + + public object GetQueryableHandlerForQueryStringParameter(Type resourceClrType, string parameterName) + { + return null; + } + + public IDictionary GetMeta(ResourceType resourceType, IIdentifiable resourceInstance) + { + return null; + } + + public Task OnPrepareWriteAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnSetToOneRelationshipAsync(TResource leftResource, HasOneAttribute hasOneRelationship, + IIdentifiable rightResourceId, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.FromResult(rightResourceId); + } + + public Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, + ISet rightResourceIds, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnAddToRelationshipAsync(TId leftResourceId, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnRemoveFromRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, + ISet rightResourceIds, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnWritingAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnWriteSucceededAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public void OnDeserialize(IIdentifiable resource) + { + } + + public void OnSerialize(IIdentifiable resource) + { + } + } + + private sealed class FakeRequestQueryStringAccessor : IRequestQueryStringAccessor + { + public IQueryCollection Query { get; } = new QueryCollection(0); + } + } +} diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseSerializationFakers.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseSerializationFakers.cs new file mode 100644 index 0000000000..acc001a16b --- /dev/null +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseSerializationFakers.cs @@ -0,0 +1,49 @@ +using Bogus; +using JsonApiDotNetCoreTests.UnitTests.Serialization.Response.Models; +using Person = JsonApiDotNetCoreTests.UnitTests.Serialization.Response.Models.Person; + +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + +namespace JsonApiDotNetCoreTests.UnitTests.Serialization.Response +{ + internal sealed class ResponseSerializationFakers + { + private const int FakerSeed = 0; + private int _index; + + public Faker Food { get; } + public Faker Song { get; } + public Faker
Article { get; } + public Faker Blog { get; } + public Faker Person { get; } + + public ResponseSerializationFakers() + { + Article = new Faker
() + .UseSeed(FakerSeed) + .RuleFor(article => article.Title, faker => faker.Hacker.Phrase()) + .RuleFor(article => article.Id, _ => ++_index); + + Person = new Faker() + .UseSeed(FakerSeed) + .RuleFor(person => person.Name, faker => faker.Person.FullName) + .RuleFor(person => person.Id, _ => ++_index); + + Blog = new Faker() + .UseSeed(FakerSeed) + .RuleFor(blog => blog.Title, faker => faker.Hacker.Phrase()) + .RuleFor(blog => blog.Id, _ => ++_index); + + Song = new Faker() + .UseSeed(FakerSeed) + .RuleFor(song => song.Title, faker => faker.Lorem.Sentence()) + .RuleFor(song => song.Id, _ => ++_index); + + Food = new Faker() + .UseSeed(FakerSeed) + .RuleFor(food => food.Dish, faker => faker.Lorem.Sentence()) + .RuleFor(food => food.Id, _ => ++_index); + } + } +} From 5dfb6eadaec2fd97ed550cf37ff10ea05c4c8d50 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 26 Oct 2021 11:44:59 +0200 Subject: [PATCH 49/49] Fixed cibuild --- .../Serialization/Response/ResourceObjectTreeNode.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs b/src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs index 3e46f43571..5137f62716 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs @@ -51,7 +51,7 @@ public ResourceObjectTreeNode(IIdentifiable resource, ResourceType type, Resourc public static ResourceObjectTreeNode CreateRoot() { - return new ResourceObjectTreeNode(RootResource, RootType, new ResourceObject()); + return new(RootResource, RootType, new ResourceObject()); } public void AttachDirectChild(ResourceObjectTreeNode treeNode) @@ -182,6 +182,8 @@ public IList GetResponseIncluded() private IList GetDirectChildren() { + // ReSharper disable once MergeConditionalExpression + // Justification: ReSharper reporting this is a bug, which is fixed in v2021.2.1. This condition cannot be merged. return _directChildren == null ? Array.Empty() : _directChildren; }