From 461d6d3926181b714ce66ca695819860d4aa5cf6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 29 May 2024 05:07:56 +0000
Subject: [PATCH 01/10] Bump dotnet-reportgenerator-globaltool from 5.3.0 to
5.3.4 (#1555)
---
.config/dotnet-tools.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 2e7b1d6d91..211c99567a 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -15,7 +15,7 @@
]
},
"dotnet-reportgenerator-globaltool": {
- "version": "5.3.0",
+ "version": "5.3.4",
"commands": [
"reportgenerator"
]
From e47faf3e52793cfcba6414d1df02eae3efe33cf2 Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Tue, 18 Jun 2024 02:22:29 +0200
Subject: [PATCH 02/10] Add IAtomicOperationFilter, which is used to constrain
the exposed atomic:operations.
---
docs/usage/writing/bulk-batch-operations.md | 15 +-
.../Controllers/OperationsController.cs | 3 +-
.../Controllers/OperationsController.cs | 3 +-
.../DefaultOperationFilter.cs | 32 ++++
.../IAtomicOperationFilter.cs | 42 +++++
.../JsonApiApplicationBuilder.cs | 1 +
.../BaseJsonApiOperationsController.cs | 71 ++++++-
.../JsonApiOperationsController.cs | 3 +-
...omConstrainedOperationsControllerTests.cs} | 18 +-
...ultConstrainedOperationsControllerTests.cs | 173 ++++++++++++++++++
.../CreateMusicTrackOperationsController.cs | 36 +---
.../AtomicOperations/OperationsController.cs | 3 +-
.../AtomicOperations/TextLanguage.cs | 4 +-
.../Scopes/OperationsController.cs | 3 +-
.../OperationsController.cs | 3 +-
15 files changed, 364 insertions(+), 46 deletions(-)
create mode 100644 src/JsonApiDotNetCore/AtomicOperations/DefaultOperationFilter.cs
create mode 100644 src/JsonApiDotNetCore/AtomicOperations/IAtomicOperationFilter.cs
rename test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/{AtomicConstrainedOperationsControllerTests.cs => AtomicCustomConstrainedOperationsControllerTests.cs} (86%)
create mode 100644 test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicDefaultConstrainedOperationsControllerTests.cs
diff --git a/docs/usage/writing/bulk-batch-operations.md b/docs/usage/writing/bulk-batch-operations.md
index 1ac35fd3fc..5756755b51 100644
--- a/docs/usage/writing/bulk-batch-operations.md
+++ b/docs/usage/writing/bulk-batch-operations.md
@@ -19,13 +19,24 @@ public sealed class OperationsController : JsonApiOperationsController
{
public OperationsController(IJsonApiOptions options, IResourceGraph resourceGraph,
ILoggerFactory loggerFactory, IOperationsProcessor processor,
- IJsonApiRequest request, ITargetedFields targetedFields)
- : base(options, resourceGraph, loggerFactory, processor, request, targetedFields)
+ IJsonApiRequest request, ITargetedFields targetedFields,
+ IAtomicOperationFilter operationFilter)
+ : base(options, resourceGraph, loggerFactory, processor, request, targetedFields,
+ operationFilter)
{
}
}
```
+> [!IMPORTANT]
+> Since v5.6.0, the set of exposed operations is based on
+> [`GenerateControllerEndpoints` usage](~/usage/extensibility/controllers.md#resource-access-control).
+> Earlier versions always exposed all operations for all resource types.
+> If you're using [explicit controllers](~/usage/extensibility/controllers.md#explicit-controllers),
+> register and implement your own
+> [`IAtomicOperationFilter`](~/api/JsonApiDotNetCore.AtomicOperations.IAtomicOperationFilter.yml)
+> to indicate which operations to expose.
+
You'll need to send the next Content-Type in a POST request for operations:
```
diff --git a/src/Examples/DapperExample/Controllers/OperationsController.cs b/src/Examples/DapperExample/Controllers/OperationsController.cs
index 6fe0eedd1d..2b9daf492f 100644
--- a/src/Examples/DapperExample/Controllers/OperationsController.cs
+++ b/src/Examples/DapperExample/Controllers/OperationsController.cs
@@ -8,4 +8,5 @@ namespace DapperExample.Controllers;
public sealed class OperationsController(
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request,
- ITargetedFields targetedFields) : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields);
+ ITargetedFields targetedFields, IAtomicOperationFilter operationFilter) : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor,
+ request, targetedFields, operationFilter);
diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/OperationsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/OperationsController.cs
index e38b30d861..9d8d944967 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Controllers/OperationsController.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/OperationsController.cs
@@ -8,4 +8,5 @@ namespace JsonApiDotNetCoreExample.Controllers;
public sealed class OperationsController(
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request,
- ITargetedFields targetedFields) : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields);
+ ITargetedFields targetedFields, IAtomicOperationFilter operationFilter) : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor,
+ request, targetedFields, operationFilter);
diff --git a/src/JsonApiDotNetCore/AtomicOperations/DefaultOperationFilter.cs b/src/JsonApiDotNetCore/AtomicOperations/DefaultOperationFilter.cs
new file mode 100644
index 0000000000..d1ec1bd65c
--- /dev/null
+++ b/src/JsonApiDotNetCore/AtomicOperations/DefaultOperationFilter.cs
@@ -0,0 +1,32 @@
+using System.Reflection;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Middleware;
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace JsonApiDotNetCore.AtomicOperations;
+
+///
+internal sealed class DefaultOperationFilter : IAtomicOperationFilter
+{
+ ///
+ public bool IsEnabled(ResourceType resourceType, WriteOperationKind writeOperation)
+ {
+ var resourceAttribute = resourceType.ClrType.GetCustomAttribute();
+ return resourceAttribute != null && Contains(resourceAttribute.GenerateControllerEndpoints, writeOperation);
+ }
+
+ private static bool Contains(JsonApiEndpoints endpoints, WriteOperationKind writeOperation)
+ {
+ return writeOperation switch
+ {
+ WriteOperationKind.CreateResource => endpoints.HasFlag(JsonApiEndpoints.Post),
+ WriteOperationKind.UpdateResource => endpoints.HasFlag(JsonApiEndpoints.Patch),
+ WriteOperationKind.DeleteResource => endpoints.HasFlag(JsonApiEndpoints.Delete),
+ WriteOperationKind.SetRelationship => endpoints.HasFlag(JsonApiEndpoints.PatchRelationship),
+ WriteOperationKind.AddToRelationship => endpoints.HasFlag(JsonApiEndpoints.PostRelationship),
+ WriteOperationKind.RemoveFromRelationship => endpoints.HasFlag(JsonApiEndpoints.DeleteRelationship),
+ _ => false
+ };
+ }
+}
diff --git a/src/JsonApiDotNetCore/AtomicOperations/IAtomicOperationFilter.cs b/src/JsonApiDotNetCore/AtomicOperations/IAtomicOperationFilter.cs
new file mode 100644
index 0000000000..240efbf936
--- /dev/null
+++ b/src/JsonApiDotNetCore/AtomicOperations/IAtomicOperationFilter.cs
@@ -0,0 +1,42 @@
+using JetBrains.Annotations;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Middleware;
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace JsonApiDotNetCore.AtomicOperations;
+
+///
+/// Determines whether an operation in an atomic:operations request can be used.
+///
+///
+/// The default implementation relies on the usage of . If you're using explicit
+/// (non-generated) controllers, register your own implementation to indicate which operations are accessible.
+///
+[PublicAPI]
+public interface IAtomicOperationFilter
+{
+ ///
+ /// An that always returns true. Provided for convenience, to revert to the original behavior from before
+ /// filtering was introduced.
+ ///
+ public static IAtomicOperationFilter AlwaysEnabled { get; } = new AlwaysEnabledOperationFilter();
+
+ ///
+ /// Determines whether the specified operation can be used in an atomic:operations request.
+ ///
+ ///
+ /// The targeted primary resource type of the operation.
+ ///
+ ///
+ /// The operation kind.
+ ///
+ bool IsEnabled(ResourceType resourceType, WriteOperationKind writeOperation);
+
+ private sealed class AlwaysEnabledOperationFilter : IAtomicOperationFilter
+ {
+ public bool IsEnabled(ResourceType resourceType, WriteOperationKind writeOperation)
+ {
+ return true;
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
index 2973a664f6..2f725e8c68 100644
--- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
+++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
@@ -300,5 +300,6 @@ private void AddOperationsLayer()
_services.TryAddScoped();
_services.TryAddScoped();
_services.TryAddScoped();
+ _services.TryAddSingleton();
}
}
diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
index 596b22794d..5485fad3e0 100644
--- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
+++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
@@ -1,9 +1,11 @@
+using System.Net;
using JetBrains.Annotations;
using JsonApiDotNetCore.AtomicOperations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
@@ -22,10 +24,11 @@ public abstract class BaseJsonApiOperationsController : CoreJsonApiController
private readonly IOperationsProcessor _processor;
private readonly IJsonApiRequest _request;
private readonly ITargetedFields _targetedFields;
+ private readonly IAtomicOperationFilter _operationFilter;
private readonly TraceLogWriter _traceWriter;
protected BaseJsonApiOperationsController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory,
- IOperationsProcessor processor, IJsonApiRequest request, ITargetedFields targetedFields)
+ IOperationsProcessor processor, IJsonApiRequest request, ITargetedFields targetedFields, IAtomicOperationFilter operationFilter)
{
ArgumentGuard.NotNull(options);
ArgumentGuard.NotNull(resourceGraph);
@@ -33,12 +36,14 @@ protected BaseJsonApiOperationsController(IJsonApiOptions options, IResourceGrap
ArgumentGuard.NotNull(processor);
ArgumentGuard.NotNull(request);
ArgumentGuard.NotNull(targetedFields);
+ ArgumentGuard.NotNull(operationFilter);
_options = options;
_resourceGraph = resourceGraph;
_processor = processor;
_request = request;
_targetedFields = targetedFields;
+ _operationFilter = operationFilter;
_traceWriter = new TraceLogWriter(loggerFactory);
}
@@ -111,6 +116,8 @@ public virtual async Task PostOperationsAsync([FromBody] IList PostOperationsAsync([FromBody] IList result != null) ? Ok(results) : NoContent();
}
+ protected virtual void ValidateEnabledOperations(IList operations)
+ {
+ List errors = [];
+
+ for (int operationIndex = 0; operationIndex < operations.Count; operationIndex++)
+ {
+ IJsonApiRequest operationRequest = operations[operationIndex].Request;
+ WriteOperationKind operationKind = operationRequest.WriteOperation!.Value;
+
+ if (operationRequest.Relationship != null && !_operationFilter.IsEnabled(operationRequest.Relationship.LeftType, operationKind))
+ {
+ string operationCode = GetOperationCodeText(operationKind);
+
+ errors.Add(new ErrorObject(HttpStatusCode.UnprocessableEntity)
+ {
+ Title = "The requested operation is not accessible.",
+ Detail = $"The '{operationCode}' relationship operation is not accessible for relationship '{operationRequest.Relationship}' " +
+ $"on resource type '{operationRequest.Relationship.LeftType}'.",
+ Source = new ErrorSource
+ {
+ Pointer = $"/atomic:operations[{operationIndex}]"
+ }
+ });
+ }
+ else if (operationRequest.PrimaryResourceType != null && !_operationFilter.IsEnabled(operationRequest.PrimaryResourceType, operationKind))
+ {
+ string operationCode = GetOperationCodeText(operationKind);
+
+ errors.Add(new ErrorObject(HttpStatusCode.UnprocessableEntity)
+ {
+ Title = "The requested operation is not accessible.",
+ Detail = $"The '{operationCode}' resource operation is not accessible for resource type '{operationRequest.PrimaryResourceType}'.",
+ Source = new ErrorSource
+ {
+ Pointer = $"/atomic:operations[{operationIndex}]"
+ }
+ });
+ }
+ }
+
+ if (errors.Count > 0)
+ {
+ throw new JsonApiException(errors);
+ }
+ }
+
+ private static string GetOperationCodeText(WriteOperationKind operationKind)
+ {
+ AtomicOperationCode operationCode = operationKind switch
+ {
+ WriteOperationKind.CreateResource => AtomicOperationCode.Add,
+ WriteOperationKind.UpdateResource => AtomicOperationCode.Update,
+ WriteOperationKind.DeleteResource => AtomicOperationCode.Remove,
+ WriteOperationKind.AddToRelationship => AtomicOperationCode.Add,
+ WriteOperationKind.SetRelationship => AtomicOperationCode.Update,
+ WriteOperationKind.RemoveFromRelationship => AtomicOperationCode.Remove,
+ _ => throw new NotSupportedException($"Unknown operation kind '{operationKind}'.")
+ };
+
+ return operationCode.ToString().ToLowerInvariant();
+ }
+
protected virtual void ValidateModelState(IList operations)
{
// We must validate the resource inside each operation manually, because they are typed as IIdentifiable.
diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs
index bc14d4886e..168800b571 100644
--- a/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs
+++ b/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs
@@ -14,7 +14,8 @@ namespace JsonApiDotNetCore.Controllers;
///
public abstract class JsonApiOperationsController(
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request,
- ITargetedFields targetedFields) : BaseJsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields)
+ ITargetedFields targetedFields, IAtomicOperationFilter operationFilter) : BaseJsonApiOperationsController(options, resourceGraph, loggerFactory, processor,
+ request, targetedFields, operationFilter)
{
///
[HttpPost]
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicCustomConstrainedOperationsControllerTests.cs
similarity index 86%
rename from test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs
rename to test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicCustomConstrainedOperationsControllerTests.cs
index e56e9119bf..51cb1a53a2 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicCustomConstrainedOperationsControllerTests.cs
@@ -6,13 +6,13 @@
namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations.Controllers;
-public sealed class AtomicConstrainedOperationsControllerTests
+public sealed class AtomicCustomConstrainedOperationsControllerTests
: IClassFixture, OperationsDbContext>>
{
private readonly IntegrationTestContext, OperationsDbContext> _testContext;
private readonly OperationsFakers _fakers = new();
- public AtomicConstrainedOperationsControllerTests(IntegrationTestContext, OperationsDbContext> testContext)
+ public AtomicCustomConstrainedOperationsControllerTests(IntegrationTestContext, OperationsDbContext> testContext)
{
_testContext = testContext;
@@ -102,14 +102,14 @@ public async Task Cannot_create_resource_for_mismatching_resource_type()
ErrorObject error = responseDocument.Errors[0];
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
- error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint.");
- error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'.");
+ error.Title.Should().Be("The requested operation is not accessible.");
+ error.Detail.Should().Be("The 'add' resource operation is not accessible for resource type 'performers'.");
error.Source.ShouldNotBeNull();
error.Source.Pointer.Should().Be("/atomic:operations[0]");
}
[Fact]
- public async Task Cannot_update_resources_for_matching_resource_type()
+ public async Task Cannot_update_resource_for_matching_resource_type()
{
// Arrange
MusicTrack existingTrack = _fakers.MusicTrack.Generate();
@@ -151,8 +151,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
ErrorObject error = responseDocument.Errors[0];
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
- error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint.");
- error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'.");
+ error.Title.Should().Be("The requested operation is not accessible.");
+ error.Detail.Should().Be("The 'update' resource operation is not accessible for resource type 'musicTracks'.");
error.Source.ShouldNotBeNull();
error.Source.Pointer.Should().Be("/atomic:operations[0]");
}
@@ -207,8 +207,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
ErrorObject error = responseDocument.Errors[0];
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
- error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint.");
- error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'.");
+ error.Title.Should().Be("The requested operation is not accessible.");
+ error.Detail.Should().Be("The 'add' relationship operation is not accessible for relationship 'performers' on resource type 'musicTracks'.");
error.Source.ShouldNotBeNull();
error.Source.Pointer.Should().Be("/atomic:operations[0]");
}
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicDefaultConstrainedOperationsControllerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicDefaultConstrainedOperationsControllerTests.cs
new file mode 100644
index 0000000000..14dc1ab83b
--- /dev/null
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicDefaultConstrainedOperationsControllerTests.cs
@@ -0,0 +1,173 @@
+using System.Net;
+using FluentAssertions;
+using JsonApiDotNetCore.Serialization.Objects;
+using TestBuildingBlocks;
+using Xunit;
+
+namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations.Controllers;
+
+public sealed class AtomicDefaultConstrainedOperationsControllerTests
+ : IClassFixture, OperationsDbContext>>
+{
+ private readonly IntegrationTestContext, OperationsDbContext> _testContext;
+ private readonly OperationsFakers _fakers = new();
+
+ public AtomicDefaultConstrainedOperationsControllerTests(IntegrationTestContext, OperationsDbContext> testContext)
+ {
+ _testContext = testContext;
+
+ testContext.UseController();
+ }
+
+ [Fact]
+ public async Task Cannot_delete_resource_for_disabled_resource_endpoint()
+ {
+ // Arrange
+ TextLanguage existingLanguage = _fakers.TextLanguage.Generate();
+
+ await _testContext.RunOnDatabaseAsync(async dbContext =>
+ {
+ dbContext.AddInRange(existingLanguage);
+ await dbContext.SaveChangesAsync();
+ });
+
+ var requestBody = new
+ {
+ atomic__operations = new[]
+ {
+ new
+ {
+ op = "remove",
+ @ref = new
+ {
+ type = "textLanguages",
+ id = existingLanguage.StringId
+ }
+ }
+ }
+ };
+
+ const string route = "/operations";
+
+ // Act
+ (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody);
+
+ // Assert
+ httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
+
+ responseDocument.Errors.ShouldHaveCount(1);
+
+ ErrorObject error = responseDocument.Errors[0];
+ error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
+ error.Title.Should().Be("The requested operation is not accessible.");
+ error.Detail.Should().Be("The 'remove' resource operation is not accessible for resource type 'textLanguages'.");
+ error.Source.ShouldNotBeNull();
+ error.Source.Pointer.Should().Be("/atomic:operations[0]");
+ }
+
+ [Fact]
+ public async Task Cannot_change_ToMany_relationship_for_disabled_resource_endpoints()
+ {
+ // Arrange
+ TextLanguage existingLanguage = _fakers.TextLanguage.Generate();
+ Lyric existingLyric = _fakers.Lyric.Generate();
+
+ await _testContext.RunOnDatabaseAsync(async dbContext =>
+ {
+ dbContext.AddInRange(existingLanguage, existingLyric);
+ await dbContext.SaveChangesAsync();
+ });
+
+ var requestBody = new
+ {
+ atomic__operations = new[]
+ {
+ new
+ {
+ op = "update",
+ @ref = new
+ {
+ type = "textLanguages",
+ id = existingLanguage.StringId,
+ relationship = "lyrics"
+ },
+ data = new[]
+ {
+ new
+ {
+ type = "lyrics",
+ id = existingLyric.StringId
+ }
+ }
+ },
+ new
+ {
+ op = "add",
+ @ref = new
+ {
+ type = "textLanguages",
+ id = existingLanguage.StringId,
+ relationship = "lyrics"
+ },
+ data = new[]
+ {
+ new
+ {
+ type = "lyrics",
+ id = existingLyric.StringId
+ }
+ }
+ },
+ new
+ {
+ op = "remove",
+ @ref = new
+ {
+ type = "textLanguages",
+ id = existingLanguage.StringId,
+ relationship = "lyrics"
+ },
+ data = new[]
+ {
+ new
+ {
+ type = "lyrics",
+ id = existingLyric.StringId
+ }
+ }
+ }
+ }
+ };
+
+ const string route = "/operations";
+
+ // Act
+ (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody);
+
+ // Assert
+ httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
+
+ responseDocument.Errors.ShouldHaveCount(3);
+
+ ErrorObject error1 = responseDocument.Errors[0];
+ error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
+ error1.Title.Should().Be("The requested operation is not accessible.");
+ error1.Detail.Should().Be("The 'update' relationship operation is not accessible for relationship 'lyrics' on resource type 'textLanguages'.");
+ error1.Source.ShouldNotBeNull();
+ error1.Source.Pointer.Should().Be("/atomic:operations[0]");
+
+ ErrorObject error2 = responseDocument.Errors[1];
+ error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
+ error2.Title.Should().Be("The requested operation is not accessible.");
+ error2.Detail.Should().Be("The 'add' relationship operation is not accessible for relationship 'lyrics' on resource type 'textLanguages'.");
+ error2.Source.ShouldNotBeNull();
+ error2.Source.Pointer.Should().Be("/atomic:operations[1]");
+
+ ErrorObject error3 = responseDocument.Errors[2];
+ error3.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
+ error3.Title.Should().Be("The requested operation is not accessible.");
+ error3.Detail.Should().Be("The 'remove' relationship operation is not accessible for relationship 'lyrics' on resource type 'textLanguages'.");
+ error3.Source.ShouldNotBeNull();
+ error3.Source.Pointer.Should().Be("/atomic:operations[2]");
+ }
+}
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs
index 01a378e5aa..b3f98df0bc 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs
@@ -1,12 +1,9 @@
-using System.Net;
using JsonApiDotNetCore.AtomicOperations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Controllers.Annotations;
-using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources;
-using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -16,35 +13,20 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations.Controllers;
[Route("/operations/musicTracks/create")]
public sealed class CreateMusicTrackOperationsController(
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request,
- ITargetedFields targetedFields) : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields)
+ ITargetedFields targetedFields) : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields,
+ OnlyCreateMusicTracksOperationFilter.Instance)
{
- public override async Task PostOperationsAsync(IList operations, CancellationToken cancellationToken)
+ private sealed class OnlyCreateMusicTracksOperationFilter : IAtomicOperationFilter
{
- AssertOnlyCreatingMusicTracks(operations);
+ public static readonly OnlyCreateMusicTracksOperationFilter Instance = new();
- return await base.PostOperationsAsync(operations, cancellationToken);
- }
-
- private static void AssertOnlyCreatingMusicTracks(IEnumerable operations)
- {
- int index = 0;
-
- foreach (OperationContainer operation in operations)
+ private OnlyCreateMusicTracksOperationFilter()
{
- if (operation.Request.WriteOperation != WriteOperationKind.CreateResource || operation.Resource.GetType() != typeof(MusicTrack))
- {
- throw new JsonApiException(new ErrorObject(HttpStatusCode.UnprocessableEntity)
- {
- Title = "Unsupported combination of operation code and resource type at this endpoint.",
- Detail = "This endpoint can only be used to create resources of type 'musicTracks'.",
- Source = new ErrorSource
- {
- Pointer = $"/atomic:operations[{index}]"
- }
- });
- }
+ }
- index++;
+ public bool IsEnabled(ResourceType resourceType, WriteOperationKind writeOperation)
+ {
+ return writeOperation == WriteOperationKind.CreateResource && resourceType.ClrType == typeof(MusicTrack);
}
}
}
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/OperationsController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/OperationsController.cs
index 5380300ede..78426804b3 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/OperationsController.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/OperationsController.cs
@@ -9,4 +9,5 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations;
public sealed class OperationsController(
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request,
- ITargetedFields targetedFields) : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields);
+ ITargetedFields targetedFields, IAtomicOperationFilter operationFilter) : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor,
+ request, targetedFields, operationFilter);
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/TextLanguage.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/TextLanguage.cs
index 02e8bf6278..e4e440600d 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/TextLanguage.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/TextLanguage.cs
@@ -1,11 +1,13 @@
using JetBrains.Annotations;
+using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations")]
+[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations",
+ GenerateControllerEndpoints = JsonApiEndpoints.Post | JsonApiEndpoints.Patch)]
public sealed class TextLanguage : Identifiable
{
[Attr]
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/OperationsController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/OperationsController.cs
index 357ff7ef5a..de1cd02c20 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/OperationsController.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/OperationsController.cs
@@ -12,7 +12,8 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.Authorization.Scopes;
public sealed class OperationsController(
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request,
- ITargetedFields targetedFields) : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields)
+ ITargetedFields targetedFields, IAtomicOperationFilter operationFilter) : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor,
+ request, targetedFields, operationFilter)
{
public override async Task PostOperationsAsync(IList operations, CancellationToken cancellationToken)
{
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/OperationsController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/OperationsController.cs
index dfe7282eac..24300dfc5c 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/OperationsController.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/OperationsController.cs
@@ -9,4 +9,5 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.ContentNegotiation;
public sealed class OperationsController(
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request,
- ITargetedFields targetedFields) : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields);
+ ITargetedFields targetedFields, IAtomicOperationFilter operationFilter) : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor,
+ request, targetedFields, operationFilter);
From c3d1f7f0eb177079fdca1a87c76596157a2b82f6 Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Wed, 19 Jun 2024 05:21:12 +0200
Subject: [PATCH 03/10] Return Forbidden when operation is inaccessible, to
match resource endpoint status code
---
.../BaseJsonApiOperationsController.cs | 4 ++--
...stomConstrainedOperationsControllerTests.cs | 18 +++++++++---------
...aultConstrainedOperationsControllerTests.cs | 16 ++++++++--------
3 files changed, 19 insertions(+), 19 deletions(-)
diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
index 5485fad3e0..b169bdd005 100644
--- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
+++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
@@ -140,7 +140,7 @@ protected virtual void ValidateEnabledOperations(IList opera
{
string operationCode = GetOperationCodeText(operationKind);
- errors.Add(new ErrorObject(HttpStatusCode.UnprocessableEntity)
+ errors.Add(new ErrorObject(HttpStatusCode.Forbidden)
{
Title = "The requested operation is not accessible.",
Detail = $"The '{operationCode}' relationship operation is not accessible for relationship '{operationRequest.Relationship}' " +
@@ -155,7 +155,7 @@ protected virtual void ValidateEnabledOperations(IList opera
{
string operationCode = GetOperationCodeText(operationKind);
- errors.Add(new ErrorObject(HttpStatusCode.UnprocessableEntity)
+ errors.Add(new ErrorObject(HttpStatusCode.Forbidden)
{
Title = "The requested operation is not accessible.",
Detail = $"The '{operationCode}' resource operation is not accessible for resource type '{operationRequest.PrimaryResourceType}'.",
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicCustomConstrainedOperationsControllerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicCustomConstrainedOperationsControllerTests.cs
index 51cb1a53a2..099168b124 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicCustomConstrainedOperationsControllerTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicCustomConstrainedOperationsControllerTests.cs
@@ -69,7 +69,7 @@ public async Task Can_create_resources_for_matching_resource_type()
}
[Fact]
- public async Task Cannot_create_resource_for_mismatching_resource_type()
+ public async Task Cannot_create_resource_for_inaccessible_operation()
{
// Arrange
var requestBody = new
@@ -96,12 +96,12 @@ public async Task Cannot_create_resource_for_mismatching_resource_type()
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody);
// Assert
- httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
+ httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden);
responseDocument.Errors.ShouldHaveCount(1);
ErrorObject error = responseDocument.Errors[0];
- error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
+ error.StatusCode.Should().Be(HttpStatusCode.Forbidden);
error.Title.Should().Be("The requested operation is not accessible.");
error.Detail.Should().Be("The 'add' resource operation is not accessible for resource type 'performers'.");
error.Source.ShouldNotBeNull();
@@ -109,7 +109,7 @@ public async Task Cannot_create_resource_for_mismatching_resource_type()
}
[Fact]
- public async Task Cannot_update_resource_for_matching_resource_type()
+ public async Task Cannot_update_resource_for_inaccessible_operation()
{
// Arrange
MusicTrack existingTrack = _fakers.MusicTrack.Generate();
@@ -145,12 +145,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody);
// Assert
- httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
+ httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden);
responseDocument.Errors.ShouldHaveCount(1);
ErrorObject error = responseDocument.Errors[0];
- error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
+ error.StatusCode.Should().Be(HttpStatusCode.Forbidden);
error.Title.Should().Be("The requested operation is not accessible.");
error.Detail.Should().Be("The 'update' resource operation is not accessible for resource type 'musicTracks'.");
error.Source.ShouldNotBeNull();
@@ -158,7 +158,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
}
[Fact]
- public async Task Cannot_add_to_ToMany_relationship_for_matching_resource_type()
+ public async Task Cannot_add_to_ToMany_relationship_for_inaccessible_operation()
{
// Arrange
MusicTrack existingTrack = _fakers.MusicTrack.Generate();
@@ -201,12 +201,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody);
// Assert
- httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
+ httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden);
responseDocument.Errors.ShouldHaveCount(1);
ErrorObject error = responseDocument.Errors[0];
- error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
+ error.StatusCode.Should().Be(HttpStatusCode.Forbidden);
error.Title.Should().Be("The requested operation is not accessible.");
error.Detail.Should().Be("The 'add' relationship operation is not accessible for relationship 'performers' on resource type 'musicTracks'.");
error.Source.ShouldNotBeNull();
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicDefaultConstrainedOperationsControllerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicDefaultConstrainedOperationsControllerTests.cs
index 14dc1ab83b..caffc32e2b 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicDefaultConstrainedOperationsControllerTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicDefaultConstrainedOperationsControllerTests.cs
@@ -20,7 +20,7 @@ public AtomicDefaultConstrainedOperationsControllerTests(IntegrationTestContext<
}
[Fact]
- public async Task Cannot_delete_resource_for_disabled_resource_endpoint()
+ public async Task Cannot_delete_resource_for_inaccessible_operation()
{
// Arrange
TextLanguage existingLanguage = _fakers.TextLanguage.Generate();
@@ -53,12 +53,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody);
// Assert
- httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
+ httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden);
responseDocument.Errors.ShouldHaveCount(1);
ErrorObject error = responseDocument.Errors[0];
- error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
+ error.StatusCode.Should().Be(HttpStatusCode.Forbidden);
error.Title.Should().Be("The requested operation is not accessible.");
error.Detail.Should().Be("The 'remove' resource operation is not accessible for resource type 'textLanguages'.");
error.Source.ShouldNotBeNull();
@@ -66,7 +66,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
}
[Fact]
- public async Task Cannot_change_ToMany_relationship_for_disabled_resource_endpoints()
+ public async Task Cannot_change_ToMany_relationship_for_inaccessible_operations()
{
// Arrange
TextLanguage existingLanguage = _fakers.TextLanguage.Generate();
@@ -145,26 +145,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody);
// Assert
- httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
+ httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden);
responseDocument.Errors.ShouldHaveCount(3);
ErrorObject error1 = responseDocument.Errors[0];
- error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
+ error1.StatusCode.Should().Be(HttpStatusCode.Forbidden);
error1.Title.Should().Be("The requested operation is not accessible.");
error1.Detail.Should().Be("The 'update' relationship operation is not accessible for relationship 'lyrics' on resource type 'textLanguages'.");
error1.Source.ShouldNotBeNull();
error1.Source.Pointer.Should().Be("/atomic:operations[0]");
ErrorObject error2 = responseDocument.Errors[1];
- error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
+ error2.StatusCode.Should().Be(HttpStatusCode.Forbidden);
error2.Title.Should().Be("The requested operation is not accessible.");
error2.Detail.Should().Be("The 'add' relationship operation is not accessible for relationship 'lyrics' on resource type 'textLanguages'.");
error2.Source.ShouldNotBeNull();
error2.Source.Pointer.Should().Be("/atomic:operations[1]");
ErrorObject error3 = responseDocument.Errors[2];
- error3.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
+ error3.StatusCode.Should().Be(HttpStatusCode.Forbidden);
error3.Title.Should().Be("The requested operation is not accessible.");
error3.Detail.Should().Be("The 'remove' relationship operation is not accessible for relationship 'lyrics' on resource type 'textLanguages'.");
error3.Source.ShouldNotBeNull();
From 8b0b90b5eb5c65c0704064105d7adfa18c35f5e2 Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Wed, 19 Jun 2024 05:29:43 +0200
Subject: [PATCH 04/10] Remove installing PowerShell in cibuild, this is no
longer needed
---
.github/workflows/build.yml | 30 ------------------------------
1 file changed, 30 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9ef1f2f119..da0993c185 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -48,36 +48,6 @@ jobs:
dotnet-version: |
6.0.x
8.0.x
- - name: Setup PowerShell (Ubuntu)
- if: matrix.os == 'ubuntu-latest'
- run: |
- dotnet tool install --global PowerShell
- - name: Find latest PowerShell version (Windows)
- if: matrix.os == 'windows-latest'
- shell: pwsh
- run: |
- $packageName = "powershell"
- $outputText = dotnet tool search $packageName --take 1
- $outputLine = ("" + $outputText)
- $indexOfVersionLine = $outputLine.IndexOf($packageName)
- $latestVersion = $outputLine.substring($indexOfVersionLine + $packageName.length).trim().split(" ")[0].trim()
-
- Write-Output "Found PowerShell version: $latestVersion"
- Write-Output "POWERSHELL_LATEST_VERSION=$latestVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- - name: Setup PowerShell (Windows)
- if: matrix.os == 'windows-latest'
- shell: cmd
- run: |
- set DOWNLOAD_LINK=https://github.com/PowerShell/PowerShell/releases/download/v%POWERSHELL_LATEST_VERSION%/PowerShell-%POWERSHELL_LATEST_VERSION%-win-x64.msi
- set OUTPUT_PATH=%RUNNER_TEMP%\PowerShell-%POWERSHELL_LATEST_VERSION%-win-x64.msi
- echo Downloading from: %DOWNLOAD_LINK% to: %OUTPUT_PATH%
- curl --location --output %OUTPUT_PATH% %DOWNLOAD_LINK%
- msiexec.exe /package %OUTPUT_PATH% /quiet USE_MU=1 ENABLE_MU=1 ADD_PATH=1 DISABLE_TELEMETRY=1
- - name: Setup PowerShell (macOS)
- if: matrix.os == 'macos-latest'
- run: |
- /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- brew install --cask powershell
- name: Show installed versions
shell: pwsh
run: |
From 81b82adab2998d887e342052d8fe5f46fad1ef27 Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Wed, 19 Jun 2024 05:43:42 +0200
Subject: [PATCH 05/10] Fixed: return empty object instead of data:null in
operation results
---
.../WriteOnlyDocumentConverter.cs | 23 ++++++++++++++++++-
.../Mixed/AtomicSerializationTests.cs | 4 +---
2 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyDocumentConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyDocumentConverter.cs
index 2597afacac..a8f3e7f81e 100644
--- a/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyDocumentConverter.cs
+++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyDocumentConverter.cs
@@ -61,7 +61,28 @@ public override void Write(Utf8JsonWriter writer, Document value, JsonSerializer
if (!value.Results.IsNullOrEmpty())
{
writer.WritePropertyName(AtomicResultsText);
- WriteSubTree(writer, value.Results, options);
+ writer.WriteStartArray();
+
+ foreach (AtomicResultObject result in value.Results)
+ {
+ writer.WriteStartObject();
+
+ if (result.Data.IsAssigned)
+ {
+ writer.WritePropertyName(DataText);
+ WriteSubTree(writer, result.Data, options);
+ }
+
+ if (!result.Meta.IsNullOrEmpty())
+ {
+ writer.WritePropertyName(MetaText);
+ WriteSubTree(writer, result.Meta, options);
+ }
+
+ writer.WriteEndObject();
+ }
+
+ writer.WriteEndArray();
}
if (!value.Errors.IsNullOrEmpty())
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs
index 4f6a2f0a3c..fc6d366f94 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs
@@ -101,9 +101,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
"self": "http://localhost/operations"
},
"atomic:results": [
- {
- "data": null
- },
+ {},
{
"data": {
"type": "textLanguages",
From efab186af97f2c5f5672319f92152f22b3cecfd8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 19 Jun 2024 11:38:24 +0000
Subject: [PATCH 06/10] Bump jetbrains.resharper.globaltools from 2024.1.2 to
2024.1.3 (#1558)
---
.config/dotnet-tools.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 211c99567a..da66b46f5f 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
- "version": "2024.1.2",
+ "version": "2024.1.3",
"commands": [
"jb"
]
From ee184d6695579228bc9f3ec5688cf11d77b28e31 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 19 Jun 2024 21:46:59 +0000
Subject: [PATCH 07/10] Bump dotnet-reportgenerator-globaltool from 5.3.4 to
5.3.6 (#1557)
---
.config/dotnet-tools.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index da66b46f5f..8be5880888 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -15,7 +15,7 @@
]
},
"dotnet-reportgenerator-globaltool": {
- "version": "5.3.4",
+ "version": "5.3.6",
"commands": [
"reportgenerator"
]
From 4f3d5bde3f559818df097b3998d21403f9213427 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 19 Jun 2024 22:17:24 +0000
Subject: [PATCH 08/10] Update Microsoft.CodeAnalysis.CSharp from 4.9.* to
4.10.* in tests (#1559)
---
package-versions.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package-versions.props b/package-versions.props
index 28e5eb5ef2..9ba0b34bbb 100644
--- a/package-versions.props
+++ b/package-versions.props
@@ -8,7 +8,7 @@
0.13.*
35.5.*
- 4.9.*
+ 4.10.*
6.0.*
2.1.*
6.12.*
From 2a4df13e7e80cd95f0e5bc84f11d6ce9a3adfbe4 Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Mon, 26 Feb 2024 02:47:13 +0100
Subject: [PATCH 09/10] Exclude kiota-lock.json from source control due to bug
in Kiota
---
.gitignore | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.gitignore b/.gitignore
index 85bd0f1080..c1757fc159 100644
--- a/.gitignore
+++ b/.gitignore
@@ -423,3 +423,6 @@ FodyWeavers.xsd
**/.idea/**/httpRequests/
**/.idea/**/dataSources/
!**/.idea/**/codeStyles/*
+
+# Workaround for https://github.com/microsoft/kiota/issues/4228
+kiota-lock.json
From 491b003f1f1980237f9904ad4dec24d4cfb3c945 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 25 Jun 2024 03:50:52 +0000
Subject: [PATCH 10/10] Bump jetbrains.resharper.globaltools from 2024.1.3 to
2024.1.4 (#1569)
---
.config/dotnet-tools.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 8be5880888..435201037d 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
- "version": "2024.1.3",
+ "version": "2024.1.4",
"commands": [
"jb"
]