Skip to content

Commit 0a3605d

Browse files
committed
documentation: hooks
1 parent c284ed9 commit 0a3605d

File tree

1 file changed

+117
-9
lines changed

1 file changed

+117
-9
lines changed

docs/usage/resources/hooks.md

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
# Resource Hooks
23
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>`.
34

@@ -13,14 +14,15 @@ Understanding the semantics will be helpful in identifying which hooks on `Resou
1314
2. [**Hook execution overview**](#hook-execution-overview)
1415
A table overview of all pipelines and involved hooks
1516
3. [**Examples: basic usage**](#examples-basic-usage)
16-
* [**Most minimal example**](#most-minimal-example)
17-
* [**Logging**](#logging)
18-
* [**Transforming data with OnReturn**](#transforming-data-with-onreturn)
17+
* [**Getting started: most minimal example**](#getting-started-most-minimal-example)
18+
* [**Logging**](#logging)
19+
* [**Transforming data with OnReturn**](#transforming-data-with-onreturn)
20+
* [**Loading database values**](#loading-database-values)
1921
5. [**Examples: advanced usage**](#examples-advanced-usage)
20-
* [**Simple authorization: explicitly affected resources**](#simple-authorization-explicitly-affected-resources)
21-
* [**Advanced authorization: implicitly affected resources**](#advanced-authorization-implicitly-affected-resources)
22-
* [**Synchronizing data across microservices**](#synchronizing-data-across-microservices)
23-
* [**Hooks for many-to-many join tables**](#hooks-for-many-to-many-join-tables)
22+
* [**Simple authorization: explicitly affected resources**](#simple-authorization-explicitly-affected-resources)
23+
* [**Advanced authorization: implicitly affected resources**](#advanced-authorization-implicitly-affected-resources)
24+
* [**Synchronizing data across microservices**](#synchronizing-data-across-microservices)
25+
* [**Hooks for many-to-many join tables**](#hooks-for-many-to-many-join-tables)
2426

2527

2628
# 1. Semantics: pipelines, actions and hooks
@@ -168,8 +170,26 @@ This table below shows the involved hooks per pipeline.
168170

169171
# 3. Examples: basic usage
170172

171-
## Most minimal example
172-
The simplest example does not require much explanation. This hook would triggered by any default JsonApiDotNetCore API route for `Article`.
173+
## Getting started: most minimal example
174+
To use resource hooks, you are required to turn them on in your `startup.cs` configuration
175+
176+
```c#
177+
public void ConfigureServices(IServiceCollection services)
178+
{
179+
...
180+
services.AddJsonApi<ApiDbContext>(
181+
options =>
182+
{
183+
options.EnableResourceHooks = true; // default is false
184+
options.LoadDatabaseValues = false; // default is true
185+
}
186+
);
187+
...
188+
}
189+
```
190+
For this example, we may set `LoadDatabaseValues` to `false`. See the [Loading database values](#loading-database-values) example for more information about this option.
191+
192+
The simplest case of resource hooks we can then implement should not require a lot of explanation. This hook would triggered by any default JsonApiDotNetCore API route for `Article`.
173193
```c#
174194
public class ArticleResource : ResourceDefinition<Article>
175195
{
@@ -294,6 +314,94 @@ public class PersonResource : ResourceDefinition<Person>
294314
```
295315
Note that not only anonymous people will be excluded when directly performing a `GET /people`, but also when included through relationships, like `GET /articles?include=author,reviewers`. Simultaneously, `if` condition that checks for `ResourcePipeline.Get` in the `PersonResource` ensures we still get expected responses from the API when eg. creating a person with `WantsPrivacy` set to true.
296316

317+
## Loading database values
318+
When a hook is executed for a particular resource, JsonApiDotNetCore can load the corresponding database values and provide them in the hooks. This can be useful for eg.
319+
* having a diff between a previous and new state of a resource (for example when updating a resource)
320+
* performing authorization rules based on the property of a resource.
321+
322+
For example, consider a scenario in with the following two requirements:
323+
* We need to log all updates on resources revealing their old and new value.
324+
* We need to check if the property `IsLocked` is set is `true`, and if so, cancel the operation.
325+
326+
Consider an `Article` with title *Hello there* and API user trying to update the the title of this article to *Bye bye*. The above requirements could be implemented as follows
327+
```c#
328+
public class ArticleResource : ResourceDefinition<Article>
329+
{
330+
private readonly ILogger _logger;
331+
private readonly IJsonApiContext _context;
332+
public constructor ArticleResource(ILogger logger, IJsonApiContext context)
333+
{
334+
_logger = logger;
335+
_context = context;
336+
}
337+
338+
public override IEnumerable<Article> BeforeUpdate(EntityDiff<Article> entityDiff, ResourcePipeline pipeline)
339+
{
340+
// PropertyGetter is a helper class that takes care of accessing the values on an instance of Article using reflection.
341+
var getter = new PropertyGetter<Article>();
342+
343+
entityDiff.RequestEntities.ForEach( requestEntity =>
344+
{
345+
346+
var databaseEntity = entityDiff.DatabaseEntities.Single( e => e.Id == a.Id);
347+
348+
if (databaseEntity.IsLocked) throw new JsonApiException(403, "Forbidden: this article is locked!")
349+
350+
foreach (var attr in _context.AttributesToUpdate)
351+
{
352+
var oldValue = getter(databaseEntity, attr);
353+
var newValue = getter(requestEntity, attr);
354+
355+
_logger.LogAttributeUpdate(oldValue, newValue)
356+
}
357+
});
358+
return entityDiff.RequestEntities;
359+
}
360+
}
361+
```
362+
363+
Note that database values are turned on by default. They can be turned of globally by configuring the startup as follows:
364+
```c#
365+
public void ConfigureServices(IServiceCollection services)
366+
{
367+
...
368+
services.AddJsonApi<ApiDbContext>(
369+
options =>
370+
{
371+
options.LoadDatabaseValues = false; // default is true
372+
}
373+
);
374+
...
375+
}
376+
```
377+
378+
The global setting can be used together with toggling the option on and off on the level of individual hooks using the `LoadDatabaseValues` attribute:
379+
```c#
380+
public class ArticleResource : ResourceDefinition<Article>
381+
{
382+
[LoadDatabaseValues(true)]
383+
public override IEnumerable<Article> BeforeUpdate(EntityDiff<Article> entityDiff, ResourcePipeline pipeline)
384+
{
385+
....
386+
}
387+
388+
[LoadDatabaseValues(false)]
389+
public override IEnumerable<string> BeforeUpdateRelationships(HashSet<string> ids, IAffectedRelationships<Article> resourcesByRelationship, ResourcePipeline pipeline)
390+
{
391+
// the entities stored in the IAffectedRelationships<Article> instance
392+
// are plain resource identifier objects when LoadDatabaseValues is turned off,
393+
// or objects loaded from the database when LoadDatabaseValues is turned on.
394+
....
395+
}
396+
}
397+
```
398+
399+
Note that there are some hooks that the `LoadDatabaseValues` option and attribute does not affect. The only hooks that are affected are:
400+
* `BeforeUpdate`
401+
* `BeforeUpdateRelationship`
402+
403+
404+
297405
# 3. Examples: advanced usage
298406

299407
## Simple authorization: explicitly affected resources

0 commit comments

Comments
 (0)