Skip to content

Commit 6a7c8d1

Browse files
committed
documentation: upgrade
1 parent 59a9359 commit 6a7c8d1

File tree

3 files changed

+79
-21
lines changed

3 files changed

+79
-21
lines changed

docs/usage/resources/hooks.md

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11

2+
23
# Resource Hooks
34
This section covers the usage of **Resource Hooks**, which is a feature of`ResourceDefinition<T>`. See the [ResourceDefinition usage guide](resource-definitions.md) for a general explanation on how to set up a `ResourceDefinition<T>`. For a quick start, jump right to the [Getting started: most minimal example](#getting-started-most-minimal-example) section.
45

@@ -12,16 +13,18 @@ This usage guide covers the following sections
1213
1. [**Semantics: pipelines, actions and hooks**](#semantics-pipelines-actions-and-hooks)
1314
Understanding the semantics will be helpful in identifying which hooks on `ResourceDefinition<T>` you need to implement for your use-case.
1415
2. [**Basic usage**](#basic-usage)
16+
Some examples to get you started.
1517
* [**Getting started: most minimal example**](#getting-started-most-minimal-example)
1618
* [**Logging**](#logging)
1719
* [**Transforming data with OnReturn**](#transforming-data-with-onreturn)
1820
* [**Loading database values**](#loading-database-values)
1921
3. [**Advanced usage**](#advanced-usage)
22+
Complicated examples that show the advanced features of hooks.
2023
* [**Simple authorization: explicitly affected resources**](#simple-authorization-explicitly-affected-resources)
2124
* [**Advanced authorization: implicitly affected resources**](#advanced-authorization-implicitly-affected-resources)
2225
* [**Synchronizing data across microservices**](#synchronizing-data-across-microservices)
2326
* [**Hooks for many-to-many join tables**](#hooks-for-many-to-many-join-tables)
24-
4. [**Hook execution overview**](#hook-execution-overview)
27+
5. [**Hook execution overview**](#hook-execution-overview)
2528
A table overview of all pipelines and involved hooks
2629

2730
# 1. Semantics: pipelines, actions and hooks
@@ -176,7 +179,7 @@ public class PersonResource : ResourceDefinition<Person>
176179
_userLogger = logService.Instance;
177180
}
178181

179-
public override void AfterUpdateRelationship(IUpdatedRelationshipHelper<Person> relationshipHelper, ResourcePipeline pipeline)
182+
public override void AfterUpdateRelationship(IAffectedRelationships<Person> resourcesByRelationship, ResourcePipeline pipeline)
180183
{
181184
var updatedRelationshipsToArticle = relationshipHelper.EntitiesRelatedTo<Article>();
182185
foreach (var updated in updatedRelationshipsToArticle)
@@ -257,30 +260,35 @@ public class ArticleResource : ResourceDefinition<Article>
257260
_context = context;
258261
}
259262

260-
public override IEnumerable<Article> BeforeUpdate(EntityDiff<Article> entityDiff, ResourcePipeline pipeline)
263+
public override IEnumerable<Article> BeforeUpdate(IEntityDiff<Article> entityDiff, ResourcePipeline pipeline)
261264
{
262265
// PropertyGetter is a helper class that takes care of accessing the values on an instance of Article using reflection.
263266
var getter = new PropertyGetter<Article>();
264-
265-
entityDiff.RequestEntities.ForEach( requestEntity =>
267+
268+
// EntityDiff<T> is a class that is like a list that contains EntityDiffPair<T> elements
269+
foreach (EntityDiffPair<Article> affected in entityDiff)
266270
{
271+
var currentDatabaseState = affected.DatabaseValue; // the current state in the database
272+
var proposedValueFromRequest = affected.Entity; // the value from the request
267273
268-
var databaseEntity = entityDiff.DatabaseEntities.Single( e => e.Id == a.Id);
274+
if (currentDatabaseState.IsLocked) throw new JsonApiException(403, "Forbidden: this article is locked!")
269275

270-
if (databaseEntity.IsLocked) throw new JsonApiException(403, "Forbidden: this article is locked!")
271-
272-
foreach (var attr in _context.AttributesToUpdate)
276+
foreach (var attr in _context.AttributesToUpdate)
273277
{
274-
var oldValue = getter(databaseEntity, attr);
275-
var newValue = getter(requestEntity, attr);
278+
var oldValue = getter(currentDatabaseState, attr);
279+
var newValue = getter(proposedValueFromRequest, attr);
276280

277281
_logger.LogAttributeUpdate(oldValue, newValue)
278282
}
279-
});
280-
return entityDiff.RequestEntities;
283+
}
284+
// You must return IEnumerable<Article> from this hook.
285+
// This means that you could reduce the set of entities that is
286+
// affected by this request, eg. by entityDiff.Entities.Where( ... );
287+
entityDiff.Entities;
281288
}
282289
}
283290
```
291+
In this case the `EntityDiffPair<T>.DatabaseValue` is `null`. If you try to access all database values at once (`EntityDiff.DatabaseValues`) when it they are turned off, an exception will be thrown.
284292

285293
Note that database values are turned on by default. They can be turned of globally by configuring the startup as follows:
286294
```c#
@@ -297,15 +305,15 @@ public void ConfigureServices(IServiceCollection services)
297305
}
298306
```
299307

300-
The global setting can be used together with toggling the option on and off on the level of individual hooks using the `LoadDatabaseValues` attribute:
308+
The global setting can be used together with per-hook configuration hooks using the `LoadDatabaseValues` attribute:
301309
```c#
302310
public class ArticleResource : ResourceDefinition<Article>
303311
{
304312
[LoadDatabaseValues(true)]
305-
public override IEnumerable<Article> BeforeUpdate(EntityDiff<Article> entityDiff, ResourcePipeline pipeline)
313+
public override IEnumerable<Article> BeforeUpdate(IEntityDiff<Article> entityDiff, ResourcePipeline pipeline)
306314
{
307315
....
308-
}
316+
}
309317

310318
[LoadDatabaseValues(false)]
311319
public override IEnumerable<string> BeforeUpdateRelationships(HashSet<string> ids, IAffectedRelationships<Article> resourcesByRelationship, ResourcePipeline pipeline)
@@ -314,13 +322,15 @@ public class ArticleResource : ResourceDefinition<Article>
314322
// are plain resource identifier objects when LoadDatabaseValues is turned off,
315323
// or objects loaded from the database when LoadDatabaseValues is turned on.
316324
....
325+
}
317326
}
318327
}
319328
```
320329

321330
Note that there are some hooks that the `LoadDatabaseValues` option and attribute does not affect. The only hooks that are affected are:
322331
* `BeforeUpdate`
323332
* `BeforeUpdateRelationship`
333+
* `BeforeDelete`
324334

325335

326336

@@ -379,7 +389,7 @@ This authorization requirement can be fulfilled as follows.
379389

380390
For checking the permissions for the explicitly affected resources, `New Article` and `Alice`, we may implement the `BeforeUpdate` hook for `Article`:
381391
```c#
382-
public override IEnumerable<Article> BeforeUpdate(EntityDiff<Article> entityDiff, ResourcePipeline pipeline)
392+
public override IEnumerable<Article> BeforeUpdate(IEntityDiff<Article> entityDiff, ResourcePipeline pipeline)
383393
{
384394
if (pipeline == ResourcePipeline.Patch)
385395
{
@@ -444,6 +454,54 @@ public override void BeforeImplicitUpdateRelationship(IAffectedRelationships<Per
444454
}
445455
}
446456
```
457+
458+
## Using Resource Hooks without EF Core
459+
460+
If you want to use Resource Hooks without EF Core, there are several things that you need to consider that need to be met. For any resource that you want to use hooks for:
461+
1. The corresponding resource repository must fully implement `IEntityReadRepository<TEntity, TId>`
462+
2. If you are using custom services, you will be responsible for injecting the `IResourceHookExecutor` service into your services and call the appropriate methods. See the [hook execution overview](#hook-execution-overview) to determine which hook should be fired in which scenario.
463+
464+
If you are required to use the `BeforeImplicitUpdateRelationship` hook (see previous example), there is an additional requirement. For this hook, given a particular relationship, JsonApiDotNetCore needs to be able to resolve the inverse relationship. For example: if `Article` has one author (a `Person`), then it needs to be able to resolve the `RelationshipAttribute` that corresponds to the inverse relationship for the `author` property. There are two approaches :
465+
466+
1. **Tell JsonApiDotNetCore how to do this only for the relevant models**. If you're using the `BeforeImplicitUpdateRelationship` hook only for a small set of models, eg only for the relationship of the example, then it is easiest to provide the `inverseNavigationProperty` as follows:
467+
```c#
468+
public class Article : Identifiable
469+
{
470+
...
471+
[HasOne("author", inverseNavigationProperty: "OwnerOfArticle")]
472+
public virtual Person Author { get; set; }
473+
...
474+
}
475+
public class Person : Identifiable
476+
{
477+
...
478+
[HasOne("article")]
479+
public virtual Article OwnerOfArticle { get; set; }
480+
...
481+
}
482+
```
483+
2. **Tell JsonApiDotNetCore how to do this in general**. For full support, you can provide JsonApiDotNetCore with a custom service implementation of the `IInverseRelationships` interface. relationship of the example, then it is easiest to provide the `inverseNavigationProperty` as follows:
484+
```c#
485+
public class CustomInverseRelationshipsResolver : IInverseRelationships
486+
{
487+
public void Resolve()
488+
{
489+
// the implementation of this method depends completely
490+
// the data access layer you're using.
491+
// It should set the RelationshipAttribute.InverseRelationship property
492+
// for all (relevant) relationships.
493+
// To have an idea of how to implement this method, see the InverseRelationships class
494+
// in the source code of JADNC:
495+
// https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/59a93590ac4f05c9c246eca9459b49e331250805/src/JsonApiDotNetCore/Internal/InverseRelationships.cs
496+
}
497+
}
498+
```
499+
This service will then be called run once at startup and take care of the metadata that is required for `BeforeImplicitUpdateRelationship` to be supported.
500+
501+
*Note: don't forget to register this singleton service with the service provider.*
502+
503+
504+
447505
## Synchronizing data across microservices
448506
If your application is built using a microservices infrastructure, it may be relevant to propagate data changes between microservices, [see this article for more information](https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/integration-event-based-microservice-communications). In this example, we will assume the implementation of an event bus and we will publish data consistency integration events using resource hooks.
449507

src/JsonApiDotNetCore/Hooks/Execution/EntityDiff.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,21 @@ namespace JsonApiDotNetCore.Hooks
99
/// <summary>
1010
/// A helper class that provides insight in what is to be updated. The
1111
/// <see cref="IEntityDiff{TEntity}.RequestEntities"/> property reflects what was parsed from the incoming request,
12-
/// where the <see cref="IEntityDiff{TEntity}.DatabaseEntities"/> reflects what is the current state in the database.
12+
/// where the <see cref="IEntityDiff{TEntity}.DatabaseValues"/> reflects what is the current state in the database.
1313
///
1414
/// Any relationships that are updated can be retrieved via the methods implemented on
1515
/// <see cref="IAffectedRelationships{TDependent}"/>.
1616
/// </summary>
1717
public interface IEntityDiff<TEntity> : IEnumerable<EntityDiffPair<TEntity>>, IAffectedResourcesBase<TEntity> where TEntity : class, IIdentifiable
1818
{
19-
HashSet<TEntity> DatabaseEntities { get; }
19+
HashSet<TEntity> DatabaseValues { get; }
2020
}
2121

2222
public class EntityDiff<TEntity> : AffectedResourcesBase<TEntity>, IEntityDiff<TEntity> where TEntity : class, IIdentifiable
2323
{
2424
private readonly HashSet<TEntity> _databaseEntities;
2525
private readonly bool _databaseValuesLoaded;
26-
public HashSet<TEntity> DatabaseEntities { get => _databaseEntities ?? ThrowNoDbValuesError(); }
26+
public HashSet<TEntity> DatabaseValues { get => _databaseEntities ?? ThrowNoDbValuesError(); }
2727

2828
internal EntityDiff(IEnumerable requestEntities,
2929
IEnumerable databaseEntities,

test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public void BeforeUpdate_NoImplicit_Without_Child_Hook_Implemented()
210210

211211
private bool TodoCheck(IEntityDiff<TodoItem> diff, string checksum)
212212
{
213-
var dbCheck = diff.DatabaseEntities.Single().Description == checksum;
213+
var dbCheck = diff.DatabaseValues.Single().Description == checksum;
214214
var reqCheck = diff.Single().Entity.Description == null;
215215
return (dbCheck && reqCheck);
216216
}

0 commit comments

Comments
 (0)