Skip to content

Commit 6362253

Browse files
author
Bart Koelman
authored
Added support for bulk/batch requests (atomic:operations) (#930)
1 parent 431541e commit 6362253

File tree

144 files changed

+18502
-158
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

144 files changed

+18502
-158
lines changed

Diff for: benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public JsonApiDeserializerBenchmarks()
3939
IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options);
4040
var targetedFields = new TargetedFields();
4141
var request = new JsonApiRequest();
42-
_jsonApiDeserializer = new RequestDeserializer(resourceGraph, new ResourceFactory(new ServiceContainer()), targetedFields, new HttpContextAccessor(), request);
42+
_jsonApiDeserializer = new RequestDeserializer(resourceGraph, new ResourceFactory(new ServiceContainer()), targetedFields, new HttpContextAccessor(), request, options);
4343
}
4444

4545
[Benchmark]

Diff for: docs/usage/toc.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
## [Creating](writing/creating.md)
1515
## [Updating](writing/updating.md)
1616
## [Deleting](writing/deleting.md)
17+
## [Bulk/batch](writing/bulk-batch-operations.md)
1718

1819
# [Resource Graph](resource-graph.md)
1920
# [Options](options.md)

Diff for: docs/usage/writing/bulk-batch-operations.md

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Bulk/batch
2+
3+
_since v4.1_
4+
5+
The [Atomic Operations](https://jsonapi.org/ext/atomic/) JSON:API extension defines
6+
how to perform multiple write operations in a linear and atomic manner.
7+
8+
Clients can send an array of operations in a single request. JsonApiDotNetCore guarantees that those
9+
operations will be processed in order and will either completely succeed or fail together.
10+
11+
On failure, the zero-based index of the failing operation is returned in the `error.source.pointer` field of the error response.
12+
13+
## Usage
14+
15+
To enable operations, add a controller to your project that inherits from `JsonApiOperationsController` or `BaseJsonApiOperationsController`:
16+
```c#
17+
public sealed class OperationsController : JsonApiOperationsController
18+
{
19+
public OperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory,
20+
IOperationsProcessor processor, IJsonApiRequest request,
21+
ITargetedFields targetedFields)
22+
: base(options, loggerFactory, processor, request, targetedFields)
23+
{
24+
}
25+
}
26+
```
27+
28+
You'll need to send the next Content-Type in a POST request for operations:
29+
```
30+
application/vnd.api+json; ext="https://jsonapi.org/ext/atomic"
31+
```
32+
33+
### Local IDs
34+
35+
Local IDs (lid) can be used to associate resources that have not yet been assigned an ID.
36+
The next example creates two resources and sets a relationship between them:
37+
38+
```json
39+
POST http://localhost/api/operations HTTP/1.1
40+
Content-Type: application/vnd.api+json;ext="https://jsonapi.org/ext/atomic"
41+
42+
{
43+
"atomic:operations": [
44+
{
45+
"op": "add",
46+
"data": {
47+
"type": "musicTracks",
48+
"lid": "id-for-i-will-survive",
49+
"attributes": {
50+
"title": "I will survive"
51+
}
52+
}
53+
},
54+
{
55+
"op": "add",
56+
"data": {
57+
"type": "performers",
58+
"lid": "id-for-gloria-gaynor",
59+
"attributes": {
60+
"artistName": "Gloria Gaynor"
61+
}
62+
}
63+
},
64+
{
65+
"op": "update",
66+
"ref": {
67+
"type": "musicTracks",
68+
"lid": "id-for-i-will-survive",
69+
"relationship": "performers"
70+
},
71+
"data": [
72+
{
73+
"type": "performers",
74+
"lid": "id-for-gloria-gaynor"
75+
}
76+
]
77+
}
78+
]
79+
}
80+
```
81+
82+
For example requests, see our suite of tests in JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.
83+
84+
## Configuration
85+
86+
The maximum number of operations per request defaults to 10, which you can change from Startup.cs:
87+
```c#
88+
services.AddJsonApi(options => options.MaximumOperationsPerRequest = 250);
89+
```
90+
Or, if you want to allow unconstrained, set it to `null` instead.
91+
92+
### Multiple controllers
93+
94+
You can register multiple operations controllers using custom routes, for example:
95+
```c#
96+
[DisableRoutingConvention, Route("/operations/musicTracks/create")]
97+
public sealed class CreateMusicTrackOperationsController : JsonApiOperationsController
98+
{
99+
public override async Task<IActionResult> PostOperationsAsync(
100+
IList<OperationContainer> operations, CancellationToken cancellationToken)
101+
{
102+
AssertOnlyCreatingMusicTracks(operations);
103+
104+
return await base.PostOperationsAsync(operations, cancellationToken);
105+
}
106+
}
107+
```
108+
109+
## Limitations
110+
111+
For our atomic:operations implementation, the next limitations apply:
112+
113+
- The `ref.href` field cannot be used. Use type/id or type/lid instead.
114+
- You cannot both assign and reference the same local ID in a single operation.
115+
- All repositories used in an operations request must implement `IRepositorySupportsTransaction` and participate in the same transaction.
116+
- If you're not using Entity Framework Core, you'll need to implement and register `IOperationsTransactionFactory` yourself.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using JsonApiDotNetCore.AtomicOperations;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Controllers;
4+
using JsonApiDotNetCore.Middleware;
5+
using JsonApiDotNetCore.Resources;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace JsonApiDotNetCoreExample.Controllers
9+
{
10+
public sealed class OperationsController : JsonApiOperationsController
11+
{
12+
public OperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory,
13+
IOperationsProcessor processor, IJsonApiRequest request, ITargetedFields targetedFields)
14+
: base(options, loggerFactory, processor, request, targetedFields)
15+
{
16+
}
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using JsonApiDotNetCore.Repositories;
5+
using Microsoft.EntityFrameworkCore;
6+
using Microsoft.EntityFrameworkCore.Storage;
7+
8+
namespace JsonApiDotNetCore.AtomicOperations
9+
{
10+
/// <summary>
11+
/// Represents an Entity Framework Core transaction in an atomic:operations request.
12+
/// </summary>
13+
public sealed class EntityFrameworkCoreTransaction : IOperationsTransaction
14+
{
15+
private readonly IDbContextTransaction _transaction;
16+
private readonly DbContext _dbContext;
17+
18+
/// <inheritdoc />
19+
public Guid TransactionId => _transaction.TransactionId;
20+
21+
public EntityFrameworkCoreTransaction(IDbContextTransaction transaction, DbContext dbContext)
22+
{
23+
_transaction = transaction ?? throw new ArgumentNullException(nameof(transaction));
24+
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
25+
}
26+
27+
/// <summary>
28+
/// Detaches all entities from the Entity Framework Core change tracker.
29+
/// </summary>
30+
public Task BeforeProcessOperationAsync(CancellationToken cancellationToken)
31+
{
32+
_dbContext.ResetChangeTracker();
33+
return Task.CompletedTask;
34+
}
35+
36+
/// <summary>
37+
/// Does nothing.
38+
/// </summary>
39+
public Task AfterProcessOperationAsync(CancellationToken cancellationToken)
40+
{
41+
return Task.CompletedTask;
42+
}
43+
44+
/// <inheritdoc />
45+
public Task CommitAsync(CancellationToken cancellationToken)
46+
{
47+
return _transaction.CommitAsync(cancellationToken);
48+
}
49+
50+
/// <inheritdoc />
51+
public ValueTask DisposeAsync()
52+
{
53+
return _transaction.DisposeAsync();
54+
}
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using JsonApiDotNetCore.Configuration;
5+
using JsonApiDotNetCore.Repositories;
6+
using Microsoft.EntityFrameworkCore;
7+
8+
namespace JsonApiDotNetCore.AtomicOperations
9+
{
10+
/// <summary>
11+
/// Provides transaction support for atomic:operation requests using Entity Framework Core.
12+
/// </summary>
13+
public sealed class EntityFrameworkCoreTransactionFactory : IOperationsTransactionFactory
14+
{
15+
private readonly IDbContextResolver _dbContextResolver;
16+
private readonly IJsonApiOptions _options;
17+
18+
public EntityFrameworkCoreTransactionFactory(IDbContextResolver dbContextResolver, IJsonApiOptions options)
19+
{
20+
_dbContextResolver = dbContextResolver ?? throw new ArgumentNullException(nameof(dbContextResolver));
21+
_options = options ?? throw new ArgumentNullException(nameof(options));
22+
}
23+
24+
/// <inheritdoc />
25+
public async Task<IOperationsTransaction> BeginTransactionAsync(CancellationToken cancellationToken)
26+
{
27+
var dbContext = _dbContextResolver.GetContext();
28+
29+
var transaction = _options.TransactionIsolationLevel != null
30+
? await dbContext.Database.BeginTransactionAsync(_options.TransactionIsolationLevel.Value,
31+
cancellationToken)
32+
: await dbContext.Database.BeginTransactionAsync(cancellationToken);
33+
34+
return new EntityFrameworkCoreTransaction(transaction, dbContext);
35+
}
36+
}
37+
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace JsonApiDotNetCore.AtomicOperations
2+
{
3+
/// <summary>
4+
/// Used to track declarations, assignments and references to local IDs an in atomic:operations request.
5+
/// </summary>
6+
public interface ILocalIdTracker
7+
{
8+
/// <summary>
9+
/// Removes all declared and assigned values.
10+
/// </summary>
11+
void Reset();
12+
13+
/// <summary>
14+
/// Declares a local ID without assigning a server-generated value.
15+
/// </summary>
16+
void Declare(string localId, string resourceType);
17+
18+
/// <summary>
19+
/// Assigns a server-generated ID value to a previously declared local ID.
20+
/// </summary>
21+
void Assign(string localId, string resourceType, string stringId);
22+
23+
/// <summary>
24+
/// Gets the server-assigned ID for the specified local ID.
25+
/// </summary>
26+
string GetValue(string localId, string resourceType);
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using JsonApiDotNetCore.AtomicOperations.Processors;
4+
using JsonApiDotNetCore.Resources;
5+
6+
namespace JsonApiDotNetCore.AtomicOperations
7+
{
8+
/// <summary>
9+
/// Retrieves a <see cref="IOperationProcessor"/> instance from the D/I container and invokes a method on it.
10+
/// </summary>
11+
public interface IOperationProcessorAccessor
12+
{
13+
/// <summary>
14+
/// Invokes <see cref="IOperationProcessor.ProcessAsync"/> on a processor compatible with the operation kind.
15+
/// </summary>
16+
Task<OperationContainer> ProcessAsync(OperationContainer operation, CancellationToken cancellationToken);
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Collections.Generic;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using JsonApiDotNetCore.Resources;
5+
6+
namespace JsonApiDotNetCore.AtomicOperations
7+
{
8+
/// <summary>
9+
/// Atomically processes a request that contains a list of operations.
10+
/// </summary>
11+
public interface IOperationsProcessor
12+
{
13+
/// <summary>
14+
/// Processes the list of specified operations.
15+
/// </summary>
16+
Task<IList<OperationContainer>> ProcessAsync(IList<OperationContainer> operations, CancellationToken cancellationToken);
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace JsonApiDotNetCore.AtomicOperations
6+
{
7+
/// <summary>
8+
/// Represents the overarching transaction in an atomic:operations request.
9+
/// </summary>
10+
public interface IOperationsTransaction : IAsyncDisposable
11+
{
12+
/// <summary>
13+
/// Identifies the active transaction.
14+
/// </summary>
15+
Guid TransactionId { get; }
16+
17+
/// <summary>
18+
/// Enables to execute custom logic before processing of an operation starts.
19+
/// </summary>
20+
Task BeforeProcessOperationAsync(CancellationToken cancellationToken);
21+
22+
/// <summary>
23+
/// Enables to execute custom logic after processing of an operation succeeds.
24+
/// </summary>
25+
Task AfterProcessOperationAsync(CancellationToken cancellationToken);
26+
27+
/// <summary>
28+
/// Commits all changes made to the underlying data store.
29+
/// </summary>
30+
Task CommitAsync(CancellationToken cancellationToken);
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
4+
namespace JsonApiDotNetCore.AtomicOperations
5+
{
6+
/// <summary>
7+
/// Provides a method to start the overarching transaction for an atomic:operations request.
8+
/// </summary>
9+
public interface IOperationsTransactionFactory
10+
{
11+
/// <summary>
12+
/// Starts a new transaction.
13+
/// </summary>
14+
Task<IOperationsTransaction> BeginTransactionAsync(CancellationToken cancellationToken);
15+
}
16+
}

0 commit comments

Comments
 (0)