Skip to content

Commit db483c0

Browse files
author
Bart Koelman
committed
Basic wire-up for write callbacks
1 parent d4269e6 commit db483c0

12 files changed

+532
-3
lines changed

src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
using System;
12
using System.Collections.Generic;
23
using JsonApiDotNetCore.Configuration;
34
using JsonApiDotNetCore.Queries.Expressions;
45
using JsonApiDotNetCore.Resources;
56
using JsonApiDotNetCore.Resources.Annotations;
7+
using JsonApiDotNetCore.Services;
68

79
namespace JsonApiDotNetCore.Queries
810
{
@@ -57,5 +59,12 @@ QueryLayer WrapLayerForSecondaryEndpoint<TId>(QueryLayer secondaryLayer, Resourc
5759
/// Builds a query for a to-many relationship with a filter to match on its left and right resource IDs.
5860
/// </summary>
5961
QueryLayer ComposeForHasMany<TId>(HasManyAttribute hasManyRelationship, TId leftId, ICollection<IIdentifiable> rightResourceIds);
62+
63+
/// <summary>
64+
/// Provides access to the request-scoped <see cref="IResourceDefinitionAccessor" /> instance. This method has been added solely to prevent introducing a
65+
/// breaking change in the <see cref="JsonApiResourceService{TResource,TId}" /> constructor and will be removed in the next major version.
66+
/// </summary>
67+
[Obsolete]
68+
IResourceDefinitionAccessor GetResourceDefinitionAccessor();
6069
}
6170
}

src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,12 @@ public QueryLayer ComposeForHasMany<TId>(HasManyAttribute hasManyRelationship, T
414414
};
415415
}
416416

417+
/// <inheritdoc />
418+
public IResourceDefinitionAccessor GetResourceDefinitionAccessor()
419+
{
420+
return _resourceDefinitionAccessor;
421+
}
422+
417423
protected virtual IReadOnlyCollection<IncludeElementExpression> GetIncludeElements(IReadOnlyCollection<IncludeElementExpression> includeElements,
418424
ResourceContext resourceContext)
419425
{

src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class EntityFrameworkCoreRepository<TResource, TId> : IResourceRepository
3535
private readonly IResourceFactory _resourceFactory;
3636
private readonly IEnumerable<IQueryConstraintProvider> _constraintProviders;
3737
private readonly TraceLogWriter<EntityFrameworkCoreRepository<TResource, TId>> _traceWriter;
38+
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;
3839

3940
/// <inheritdoc />
4041
public virtual string TransactionId => _dbContext.Database.CurrentTransaction?.TransactionId.ToString();
@@ -55,6 +56,10 @@ public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextR
5556
_constraintProviders = constraintProviders;
5657
_dbContext = contextResolver.GetContext();
5758
_traceWriter = new TraceLogWriter<EntityFrameworkCoreRepository<TResource, TId>>(loggerFactory);
59+
60+
#pragma warning disable 612 // Method is obsolete
61+
_resourceDefinitionAccessor = resourceFactory.GetResourceDefinitionAccessor();
62+
#pragma warning restore 612
5863
}
5964

6065
/// <inheritdoc />
@@ -175,6 +180,8 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r
175180
attribute.SetValue(resourceForDatabase, attribute.GetValue(resourceFromRequest));
176181
}
177182

183+
await _resourceDefinitionAccessor.OnBeforeCreateResourceAsync(resourceForDatabase, cancellationToken);
184+
178185
DbSet<TResource> dbSet = _dbContext.Set<TResource>();
179186
await dbSet.AddAsync(resourceForDatabase, cancellationToken);
180187

@@ -216,6 +223,8 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r
216223
attribute.SetValue(resourceFromDatabase, attribute.GetValue(resourceFromRequest));
217224
}
218225

226+
await _resourceDefinitionAccessor.OnBeforeUpdateResourceAsync(resourceFromDatabase, cancellationToken);
227+
219228
await SaveChangesAsync(cancellationToken);
220229
}
221230

src/JsonApiDotNetCore/Resources/IResourceDefinition.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Collections.Generic;
22
using System.Linq;
3+
using System.Threading;
4+
using System.Threading.Tasks;
35
using JetBrains.Annotations;
46
using JsonApiDotNetCore.Queries.Expressions;
57

@@ -29,6 +31,7 @@ public interface IResourceDefinition<TResource> : IResourceDefinition<TResource,
2931
/// The resource identifier type.
3032
/// </typeparam>
3133
[PublicAPI]
34+
// ReSharper disable once TypeParameterCanBeVariant -- Justification: making TId contravariant is a breaking change.
3235
public interface IResourceDefinition<TResource, TId>
3336
where TResource : class, IIdentifiable<TId>
3437
{
@@ -129,5 +132,106 @@ public interface IResourceDefinition<TResource, TId>
129132
/// Enables to add JSON:API meta information, specific to this resource.
130133
/// </summary>
131134
IDictionary<string, object> GetMeta(TResource resource);
135+
136+
/// <summary>
137+
/// Enables to execute custom logic to initialize a newly instantiated resource during a POST request. This is typically used to assign default values to
138+
/// properties or to side-load-and-attach required relationships.
139+
/// </summary>
140+
/// <param name="resource">
141+
/// A freshly instantiated resource object.
142+
/// </param>
143+
/// <param name="cancellationToken">
144+
/// Propagates notification that request handling should be canceled.
145+
/// </param>
146+
Task OnInitializeResourceAsync(TResource resource, CancellationToken cancellationToken);
147+
148+
/// <summary>
149+
/// Enables to execute custom logic, just before a resource is inserted in the underlying data store, during a POST request. This is typically used to
150+
/// overwrite attributes from the incoming request, such as a creation-timestamp. Another use case is to add a notification message to an outbox table,
151+
/// which gets committed along with the resource write in a single transaction (see https://microservices.io/patterns/data/transactional-outbox.html).
152+
/// </summary>
153+
/// <param name="resource">
154+
/// The resource with incoming request data applied on it.
155+
/// </param>
156+
/// <param name="cancellationToken">
157+
/// Propagates notification that request handling should be canceled.
158+
/// </param>
159+
Task OnBeforeCreateResourceAsync(TResource resource, CancellationToken cancellationToken);
160+
161+
/// <summary>
162+
/// Enables to execute custom logic after a resource has been inserted in the underlying data store, during a POST request. A typical use case is to
163+
/// enqueue a notification message on a service bus.
164+
/// </summary>
165+
/// <param name="resource">
166+
/// The re-fetched resource after a successful insertion.
167+
/// </param>
168+
/// <param name="cancellationToken">
169+
/// Propagates notification that request handling should be canceled.
170+
/// </param>
171+
Task OnAfterCreateResourceAsync(TResource resource, CancellationToken cancellationToken);
172+
173+
/// <summary>
174+
/// Enables to execute custom logic to validate if the update request can be processed, based on the currently stored resource. A typical use case is to
175+
/// throw when the resource is soft-deleted or archived.
176+
/// </summary>
177+
/// <param name="resource">
178+
/// The resource as currently stored in the underlying data store.
179+
/// </param>
180+
/// <param name="cancellationToken">
181+
/// Propagates notification that request handling should be canceled.
182+
/// </param>
183+
Task OnAfterGetForUpdateResourceAsync(TResource resource, CancellationToken cancellationToken);
184+
185+
/// <summary>
186+
/// Enables to execute custom logic, just before a resource is updated in the underlying data store, during a PATCH request. This is typically used to
187+
/// overwrite attributes from the incoming request, such as a last-modification-timestamp. Another use case is to add a notification message to an outbox
188+
/// table, which gets committed along with the resource write in a single transaction (see
189+
/// https://microservices.io/patterns/data/transactional-outbox.html).
190+
/// </summary>
191+
/// <param name="resource">
192+
/// The stored resource with incoming request data applied on it.
193+
/// </param>
194+
/// <param name="cancellationToken">
195+
/// Propagates notification that request handling should be canceled.
196+
/// </param>
197+
Task OnBeforeUpdateResourceAsync(TResource resource, CancellationToken cancellationToken);
198+
199+
/// <summary>
200+
/// Enables to execute custom logic after a resource has been updated in the underlying data store, during a PATCH request. A typical use case is to
201+
/// enqueue a notification message on a service bus.
202+
/// </summary>
203+
/// <param name="resource">
204+
/// The re-fetched resource after a successful update.
205+
/// </param>
206+
/// <param name="cancellationToken">
207+
/// Propagates notification that request handling should be canceled.
208+
/// </param>
209+
Task OnAfterUpdateResourceAsync(TResource resource, CancellationToken cancellationToken);
210+
211+
/// <summary>
212+
/// Enables to execute custom logic, just before a resource is deleted from the underlying data store, during a DELETE request. This enables to throw in
213+
/// case the user does not have permission, an attempt is made to delete an unarchived resource or a non-closed work item etc. Another use case is to add
214+
/// a notification message to an outbox table, which gets committed along with the resource write in a single transaction (see
215+
/// https://microservices.io/patterns/data/transactional-outbox.html).
216+
/// </summary>
217+
/// <param name="id">
218+
/// The identifier of the resource to delete.
219+
/// </param>
220+
/// <param name="cancellationToken">
221+
/// Propagates notification that request handling should be canceled.
222+
/// </param>
223+
Task OnBeforeDeleteResourceAsync(TId id, CancellationToken cancellationToken);
224+
225+
/// <summary>
226+
/// Enables to execute custom logic after a resource has been deleted from the underlying data store, during a DELETE request. A typical use case is to
227+
/// enqueue a notification message on a service bus.
228+
/// </summary>
229+
/// <param name="id">
230+
/// The identifier of the resource to delete.
231+
/// </param>
232+
/// <param name="cancellationToken">
233+
/// Propagates notification that request handling should be canceled.
234+
/// </param>
235+
Task OnAfterDeleteResourceAsync(TId id, CancellationToken cancellationToken);
132236
}
133237
}

src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
46
using JsonApiDotNetCore.Queries.Expressions;
57

68
namespace JsonApiDotNetCore.Resources
@@ -45,5 +47,53 @@ public interface IResourceDefinitionAccessor
4547
/// Invokes <see cref="IResourceDefinition{TResource,TId}.GetMeta" /> for the specified resource.
4648
/// </summary>
4749
IDictionary<string, object> GetMeta(Type resourceType, IIdentifiable resourceInstance);
50+
51+
/// <summary>
52+
/// Invokes <see cref="IResourceDefinition{TResource,TId}.OnInitializeResourceAsync" /> for the specified resource.
53+
/// </summary>
54+
Task OnInitializeResourceAsync<TResource>(TResource resource, CancellationToken cancellationToken)
55+
where TResource : class, IIdentifiable;
56+
57+
/// <summary>
58+
/// Invokes <see cref="IResourceDefinition{TResource,TId}.OnBeforeCreateResourceAsync" /> for the specified resource.
59+
/// </summary>
60+
Task OnBeforeCreateResourceAsync<TResource>(TResource resource, CancellationToken cancellationToken)
61+
where TResource : class, IIdentifiable;
62+
63+
/// <summary>
64+
/// Invokes <see cref="IResourceDefinition{TResource,TId}.OnAfterCreateResourceAsync" /> for the specified resource.
65+
/// </summary>
66+
Task OnAfterCreateResourceAsync<TResource>(TResource resource, CancellationToken cancellationToken)
67+
where TResource : class, IIdentifiable;
68+
69+
/// <summary>
70+
/// Invokes <see cref="IResourceDefinition{TResource,TId}.OnAfterGetForUpdateResourceAsync" /> for the specified resource.
71+
/// </summary>
72+
Task OnAfterGetForUpdateResourceAsync<TResource>(TResource resource, CancellationToken cancellationToken)
73+
where TResource : class, IIdentifiable;
74+
75+
/// <summary>
76+
/// Invokes <see cref="IResourceDefinition{TResource,TId}.OnBeforeUpdateResourceAsync" /> for the specified resource.
77+
/// </summary>
78+
Task OnBeforeUpdateResourceAsync<TResource>(TResource resource, CancellationToken cancellationToken)
79+
where TResource : class, IIdentifiable;
80+
81+
/// <summary>
82+
/// Invokes <see cref="IResourceDefinition{TResource,TId}.OnAfterUpdateResourceAsync" /> for the specified resource.
83+
/// </summary>
84+
Task OnAfterUpdateResourceAsync<TResource>(TResource resource, CancellationToken cancellationToken)
85+
where TResource : class, IIdentifiable;
86+
87+
/// <summary>
88+
/// Invokes <see cref="IResourceDefinition{TResource,TId}.OnBeforeDeleteResourceAsync" /> for the specified resource ID.
89+
/// </summary>
90+
Task OnBeforeDeleteResourceAsync<TResource, TId>(TId id, CancellationToken cancellationToken)
91+
where TResource : class, IIdentifiable<TId>;
92+
93+
/// <summary>
94+
/// Invokes <see cref="IResourceDefinition{TResource,TId}.OnAfterDeleteResourceAsync" /> for the specified resource ID.
95+
/// </summary>
96+
Task OnAfterDeleteResourceAsync<TResource, TId>(TId id, CancellationToken cancellationToken)
97+
where TResource : class, IIdentifiable<TId>;
4898
}
4999
}

src/JsonApiDotNetCore/Resources/IResourceFactory.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Linq.Expressions;
3+
using JsonApiDotNetCore.Repositories;
34

45
namespace JsonApiDotNetCore.Resources
56
{
@@ -23,5 +24,12 @@ public TResource CreateInstance<TResource>()
2324
/// Returns an expression tree that represents creating a new resource object instance.
2425
/// </summary>
2526
public NewExpression CreateNewExpression(Type resourceType);
27+
28+
/// <summary>
29+
/// Provides access to the request-scoped <see cref="IResourceDefinitionAccessor" /> instance. This method has been added solely to prevent introducing a
30+
/// breaking change in the <see cref="EntityFrameworkCoreRepository{TResource,TId}" /> constructor and will be removed in the next major version.
31+
/// </summary>
32+
[Obsolete]
33+
IResourceDefinitionAccessor GetResourceDefinitionAccessor();
2634
}
2735
}

src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using System.ComponentModel;
44
using System.Linq;
55
using System.Linq.Expressions;
6+
using System.Threading;
7+
using System.Threading.Tasks;
68
using JetBrains.Annotations;
79
using JsonApiDotNetCore.Configuration;
810
using JsonApiDotNetCore.Queries.Expressions;
@@ -113,6 +115,54 @@ public virtual IDictionary<string, object> GetMeta(TResource resource)
113115
return null;
114116
}
115117

118+
/// <inheritdoc />
119+
public virtual Task OnInitializeResourceAsync(TResource resource, CancellationToken cancellationToken)
120+
{
121+
return Task.CompletedTask;
122+
}
123+
124+
/// <inheritdoc />
125+
public virtual Task OnBeforeCreateResourceAsync(TResource resource, CancellationToken cancellationToken)
126+
{
127+
return Task.CompletedTask;
128+
}
129+
130+
/// <inheritdoc />
131+
public virtual Task OnAfterCreateResourceAsync(TResource resource, CancellationToken cancellationToken)
132+
{
133+
return Task.CompletedTask;
134+
}
135+
136+
/// <inheritdoc />
137+
public virtual Task OnAfterGetForUpdateResourceAsync(TResource resource, CancellationToken cancellationToken)
138+
{
139+
return Task.CompletedTask;
140+
}
141+
142+
/// <inheritdoc />
143+
public virtual Task OnBeforeUpdateResourceAsync(TResource resource, CancellationToken cancellationToken)
144+
{
145+
return Task.CompletedTask;
146+
}
147+
148+
/// <inheritdoc />
149+
public virtual Task OnAfterUpdateResourceAsync(TResource resource, CancellationToken cancellationToken)
150+
{
151+
return Task.CompletedTask;
152+
}
153+
154+
/// <inheritdoc />
155+
public virtual Task OnBeforeDeleteResourceAsync(TId id, CancellationToken cancellationToken)
156+
{
157+
return Task.CompletedTask;
158+
}
159+
160+
/// <inheritdoc />
161+
public virtual Task OnAfterDeleteResourceAsync(TId id, CancellationToken cancellationToken)
162+
{
163+
return Task.CompletedTask;
164+
}
165+
116166
/// <summary>
117167
/// This is an alias type intended to simplify the implementation's method signature. See <see cref="CreateSortExpressionFromLambda" /> for usage
118168
/// details.

0 commit comments

Comments
 (0)