diff --git a/docs/usage/extensibility/services.md b/docs/usage/extensibility/services.md index 5b9beb5fdd..2e4dcd43dc 100644 --- a/docs/usage/extensibility/services.md +++ b/docs/usage/extensibility/services.md @@ -18,10 +18,9 @@ public class TodoItemService : JsonApiResourceService public TodoItemService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, - IResourceChangeTracker resourceChangeTracker, - IResourceHookExecutorFacade hookExecutor) + IResourceChangeTracker resourceChangeTracker) : base(repositoryAccessor, queryLayerComposer, paginationContext, options, - loggerFactory, request, resourceChangeTracker, hookExecutor) + loggerFactory, request, resourceChangeTracker) { _notificationService = notificationService; } diff --git a/docs/usage/resources/hooks.md b/docs/usage/resources/hooks.md deleted file mode 100644 index e34c4922fd..0000000000 --- a/docs/usage/resources/hooks.md +++ /dev/null @@ -1,771 +0,0 @@ - - -# Resource Hooks -This section covers the usage of **Resource Hooks**, which is a feature of`ResourceHooksDefinition`. See the [ResourceDefinition usage guide](~/usage/extensibility/resource-definitions.md) for a general explanation on how to set up a `JsonApiResourceDefinition`. For a quick start, jump right to the [Getting started: most minimal example](#getting-started-most-minimal-example) section. - -> Note: Resource Hooks are an experimental feature and are turned off by default. They are subject to change or be replaced in a future version. - -By implementing resource hooks on a `ResourceHooksDefintion`, it is possible to intercept the execution of the **Resource Service Layer** (RSL) in various ways. This enables the developer to conveniently define business logic without having to override the RSL. It can be used to implement e.g. -* Authorization -* [Event-based synchronisation between microservices](https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/integration-event-based-microservice-communications) -* Logging -* Transformation of the served data - -This usage guide covers the following sections -1. [**Semantics: pipelines, actions and hooks**](#1-semantics-pipelines-actions-and-hooks) -Understanding the semantics will be helpful in identifying which hooks on `ResourceHooksDefinition` you need to implement for your use-case. -2. [**Basic usage**](#2-basic-usage) - Some examples to get you started. - * [**Getting started: most minimal example**](#getting-started-most-minimal-example) - * [**Logging**](#logging) - * [**Transforming data with OnReturn**](#transforming-data-with-onreturn) - * [**Loading database values**](#loading-database-values) -3. [**Advanced usage**](#3-advanced-usage) - Complicated examples that show the advanced features of hooks. - * [**Simple authorization: explicitly affected resources**](#simple-authorization-explicitly-affected-resources) - * [**Advanced authorization: implicitly affected resources**](#advanced-authorization-implicitly-affected-resources) - * [**Synchronizing data across microservices**](#synchronizing-data-across-microservices) - * [**Hooks for many-to-many join tables**](#hooks-for-many-to-many-join-tables) -5. [**Hook execution overview**](#4-hook-execution-overview) - A table overview of all pipelines and involved hooks - -# 1. Semantics: pipelines, actions and hooks - -## Pipelines -The different execution flows within the RSL that may be intercepted can be identified as **pipelines**. Examples of such pipelines are -* **Post**: creation of a resource (triggered by the endpoint `POST /my-resource`). -* **PostBulk**: creation of multiple resources (triggered by the endpoint `POST /bulk/my-resource`). - * *NB: hooks are not yet supported with bulk operations.* -* **Get**: reading a resource (triggered by the endpoint `GET /my-resource`). -* **GetSingle**: reading a single resource (triggered by the endpoint `GET /my-resource/1`). - -See the [ResourcePipeline](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/745d2fb6b6c9dd21ff794284a193977fdc699fe6/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs) enum for a full list of available pipelines. - -## Actions -Each pipeline is associated with a set of **actions** that work on resources and their relationships. These actions reflect the associated database operations that are performed by JsonApiDotNetCore (in the Repository Layer). Typically, the RSL will execute some service-layer-related code, then invoke the Repository Layer which will perform these actions, after which the execution returns to the RSL. - -Note that some actions are shared across different pipelines, and note that most pipelines perform multiple actions. There are two types of actions: **primary resource actions** and **nested resource actions**. - -### Primary resource actions -Most actions are trivial in the context of the pipeline where they're executed from. They may be recognised as the familiar *CRUD* operations of an API. These actions are: - -* The `create` action: the **Post** pipeline will `create` a resource -* The `read` action: the **Get** and **GetSingle** pipeline will `read` (a) resource(s). -* The `update` action: the **Patch** pipeline will `update` a resource. -* The `delete` action: the **Delete** pipeline will `delete` a resource. - -These actions are called the **primary resource actions** of a particular pipeline because **they act on the request resource**. For example, when an `Article` is created through the **Post** pipeline, its main action, `create`, will work on that `Article`. - -### Nested Resource Actions -Some other actions might be overlooked, namely the nested resource actions. These actions are - -* `update relationship` for directly affected relationships -* `implicit update relationship` for implicitly affected relationships -* `read` for included relationships - -These actions are called **nested resource actions** of a particular pipeline because **they act on involved (nested) resources** instead of the primary request resource. For example, when loading articles and their respective authors (`GET /articles?include=author`), the `read` action on `Article` is the primary action, and the `read` action on `Person` is the nested action. - -#### The `update relationship` action -[As per the Json:Api specification](https://jsonapi.org/format/#crud-creating](https://jsonapi.org/format/#crud-creating), the **Post** pipeline also allows for an `update relationship` action on an already existing resource. For example, when creating an `Article` it is possible to simultaneously relate it to an existing `Person` by setting its author. In this case, the `update relationship` action is a nested action that will work on that `Person`. - -#### The `implicit update relationship` action -the **Delete** pipeline also allows for an `implicit update relationship` action on an already existing resource. For example, for an `Article` that its author property assigned to a particular `Person`, the relationship between them is destroyed when this article is deleted. This update is "implicit" in the sense that no explicit information about that `Person` was provided in the request that triggered this pipeline. An `implicit update relationship` action is therefore performed on that `Person`. See [this section](#advanced-authorization-implicitly-affected-resources) for a more detailed explanation. - -### Shared actions -Note that **some actions are shared across pipelines**. For example, both the **Post** and **Patch** pipeline can perform the `update relationship` action on an (already existing) involved resource. Similarly, the **Get** and **GetSingle** pipelines perform the same `read` action. -

-For a complete list of actions associated with each pipeline, see the [overview table](#4-hook-execution-overview). - -## Hooks -For all actions it is possible to implement **at least one hook** to intercept its execution. These hooks can be implemented by overriding the corresponding virtual implementation on `ResourceHooksDefintion`. (Note that the base implementation is a dummy implementation, which is ignored when firing hooks.) - -### Action related hooks -As an example, consider the `create` action for the `Article` Resource. This action can be intercepted by overriding the -* `ResourceHooksDefinition
.BeforeCreate` hook for custom logic **just before** execution of the main `create` action -* `ResourceHooksDefinition
.AfterCreate` hook for custom logic **just after** execution of the main `create` action - -If with the creation of an `Article` a relationship to `Person` is updated simultaneously, this can be intercepted by overriding the -* `ResourceHooksDefinition.BeforeUpdateRelationship` hook for custom logic **just before** the execution of the nested `update relationship` action. -* `ResourceHooksDefinition.AfterUpdateRelationship` hook for custom logic **just after** the execution of the nested `update relationship` action. - -### OnReturn hook -As mentioned in the previous section, some actions are shared across hooks. One of these actions is the `return` action. Although not strictly compatible with the *CRUD* vocabulary, and although not executed by the Repository Layer, pipelines are also said to perform a `return` action when any content is to be returned from the API. For example, the **Delete** pipeline does not return any content, but a *HTTP 204 No Content* instead, and will therefore not perform a `return` action. On the contrary, the **Get** pipeline does return content, and will therefore perform a `return action` - -Any return content can be intercepted and transformed as desired by implementing the `ResourceHooksDefinition.OnReturn` hook which intercepts the `return` action. For this action, there is no distinction between a `Before` and `After` hook, because no code after a `return` statement can be evaluated. Note that the `return` action can work on *primary resources as well as nested resources*, see [this example below](#transforming-data-with-onreturn). -

-For an overview of all pipelines, hooks and actions, see the table below, and for more detailed information about the available hooks, see the [IResourceHookContainer](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/ab1f96d8255532461da47d290c5440b9e7e6a4a5/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs) interface. - -# 2. Basic usage - -## Getting started: most minimal example -To use resource hooks, you are required to turn them on in your `startup.cs` configuration - -```c# -public void ConfigureServices(IServiceCollection services) -{ - // ... - - services.AddJsonApi(options => - { - options.EnableResourceHooks = true; // default is false - options.LoadDatabaseValues = false; // default is false - }); - - // ... -} -``` - -For this example, we may set `LoadDatabaseValues` to `false`. See the [Loading database values](#loading-database-values) example for more information about this option. - -The simplest case of resource hooks we can then implement should not require a lot of explanation. This hook would be triggered by any default JsonApiDotNetCore API route for `Article`. - -```c# -public class ArticleResource : ResourceHooksDefinition
-{ - public override IEnumerable
OnReturn(HashSet
entities, - ResourcePipeline pipeline) - { - Console.WriteLine("This hook does not do much apart from writing this message" + - " to the console just before serving the content."); - return entities; - } -} -``` - -## Logging -This example shows how some actions can be logged on the level of API users. - -First consider the following scoped service which creates a logger bound to a particular user and request. - -```c# -/// This is a scoped service, which means log will have a request-based -/// unique id associated to it. -public class UserActionsLogger : IUserActionsLogger -{ - public ILogger Instance { get; private set; } - - public UserActionsLogger(ILoggerFactory loggerFactory, IUserService userService) - { - var userId = userService.GetUser().Id; - Instance = - loggerFactory.CreateLogger($"[request: {Guid.NewGuid()}" + "user: {userId}]"); - } -} -``` - -Now, let's assume our application has two resources: `Article` and `Person`, and that there exist a one-to-one and one-to-many relationship between them (`Article` has one `Author` and `Article` has many `Reviewers`). Let's assume we are required to log the following events: -* An API user deletes an article -* An API user removes the `Author` relationship of a person -* An API user removes the `Reviewer` relationship of a person - -This could be achieved in the following way: - -```c# -/// Note that resource definitions are also registered as scoped services. -public class ArticleResource : ResourceHooksDefinition
-{ - private readonly ILogger _userLogger; - - public ArticleResource(IUserActionsLogger logService) - { - _userLogger = logService.Instance; - } - - public override void AfterDelete(HashSet
entities, ResourcePipeline pipeline, - bool succeeded) - { - if (!succeeded) - { - return; - } - - foreach (Article article in entities) - { - _userLogger.Log(LogLevel.Information, - $"Deleted article '{article.Name}' with id {article.Id}"); - } - } -} - -public class PersonResource : ResourceHooksDefinition -{ - private readonly ILogger _userLogger; - - public PersonResource(IUserActionsLogger logService) - { - _userLogger = logService.Instance; - } - - public override void AfterUpdateRelationship( - IAffectedRelationships resourcesByRelationship, ResourcePipeline pipeline) - { - var updatedRelationshipsToArticle = relationshipHelper.EntitiesRelatedTo
(); - - foreach (var updated in updatedRelationshipsToArticle) - { - RelationshipAttribute relationship = updated.Key; - HashSet affectedEntities = updated.Value; - - foreach (Person person in affectedEntities) - { - if (pipeline == ResourcePipeline.Delete) - { - _userLogger.Log(LogLevel.Information, - $"Deleted the {relationship.PublicRelationshipName} relationship " + - $"to Article for person '{person.FirstName} {person.LastName}' " + - $"with id {person.Id}"); - } - } - } - } -} -``` - -If eg. an API user deletes an article with title *JSON:API paints my bikeshed!* that had related as author *John Doe* and as reviewer *Frank Miller*, the logs generated logs would look something like - -``` -[request: 186190e3-1900-4329-9181-42082258e7b4, user: dd1cd99d-60e9-45ca-8d03-a0330b07bdec] Deleted article 'JSON:API paints my bikeshed!' with id fac0436b-7aa5-488e-9de7-dbe00ff8f04d -[request: 186190e3-1900-4329-9181-42082258e7b4, user: dd1cd99d-60e9-45ca-8d03-a0330b07bdec] Deleted the author relationship to Article for person 'John Doe' with id 2ec3990d-c816-4d6d-8531-7da4a030d4d0 -[request: 186190e3-1900-4329-9181-42082258e7b4, user: dd1cd99d-60e9-45ca-8d03-a0330b07bdec] Deleted the reviewer relationship to Article for person 'Frank Miller' with id 42ad6eb2-b813-4261-8fc1-0db1233e665f -``` - -## Transforming data with OnReturn -Using the `OnReturn` hook, any set of resources can be manipulated as desired before serving it from the API. One of the use-cases for this is being able to perform a [filtered include](https://github.com/aspnet/EntityFrameworkCore/issues/1833), which is currently not supported by Entity Framework Core. - -As an example, consider again an application with the `Article` and `Person` resource, and let's assume the following business rules: -* when reading `Article`s, we never want to show articles for which the `IsSoftDeleted` property is set to true. -* when reading `Person`s, we never want to show people who wish to remain anonymous (`IsAnonymous` is set to true). - -This can be achieved as follows: - -```c# -public class ArticleResource : ResourceHooksDefinition
-{ - public override IEnumerable
OnReturn(HashSet
entities, - ResourcePipeline pipeline) - { - return entities.Where(article => !article.IsSoftDeleted); - } -} - -public class PersonResource : ResourceHooksDefinition -{ - public override IEnumerable OnReturn(HashSet entities, - ResourcePipeline pipeline) - { - if (pipeline == ResourcePipeline.Get) - { - return entities.Where(person => !person.IsAnonymous); - } - return entities; - } -} -``` - -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. - -## Loading database values -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. - * having a diff between a previous and new state of a resource (for example when updating a resource) - * performing authorization rules based on the property of a resource. - -For example, consider a scenario with the following two requirements: -* We need to log all updates on resources revealing their old and new value. -* We need to check if the property `IsLocked` is set is `true`, and if so, cancel the operation. - -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: - -```c# -public class ArticleResource : ResourceHooksDefinition
-{ - private readonly ILogger _logger; - private readonly ITargetedFields _targetedFields; - - public constructor ArticleResource(ILogger logger, ITargetedFields targetedFields) - { - _logger = logger; - _targetedFields = targetedFields; - } - - public override IEnumerable
BeforeUpdate(IResourceDiff
entityDiff, - ResourcePipeline pipeline) - { - // PropertyGetter is a helper class that takes care of accessing the values - // on an instance of Article using reflection. - var getter = new PropertyGetter
(); - - // ResourceDiff is like a list that contains ResourceDiffPair elements - foreach (ResourceDiffPair
affected in entityDiff) - { - // the current state in the database - var currentDatabaseState = affected.DatabaseValue; - - // the value from the request - var proposedValueFromRequest = affected.Entity; - - if (currentDatabaseState.IsLocked) - { - throw new JsonApiException(403, "Forbidden: this article is locked!") - } - - foreach (var attr in _targetedFields.Attributes) - { - var oldValue = getter(currentDatabaseState, attr); - var newValue = getter(proposedValueFromRequest, attr); - - _logger.LogAttributeUpdate(oldValue, newValue) - } - } - - // You must return IEnumerable
from this hook. - // This means that you could reduce the set of entities that is - // affected by this request, eg. by entityDiff.Entities.Where( ... ); - entityDiff.Entities; - } -} -``` - -In this case the `ResourceDiffPair.DatabaseValue` is `null`. If you try to access all database values at once (`ResourceDiff.DatabaseValues`) when it is turned off, an exception will be thrown. - -Note that database values are turned off by default. They can be turned on globally by configuring the startup as follows: - -```c# -public void ConfigureServices(IServiceCollection services) -{ - // ... - - services.AddJsonApi(options => - { - options.LoadDatabaseValues = true; - }); - - // ... -} -``` - -The global setting can be used together with per-hook configuration hooks using the `LoadDatabaseValues` attribute: - -```c# -public class ArticleResource : ResourceHooksDefinition
-{ - [LoadDatabaseValues(true)] - public override IEnumerable
BeforeUpdate(IResourceDiff
entityDiff, - ResourcePipeline pipeline) - { - // ... - } - - [LoadDatabaseValues(false)] - public override IEnumerable BeforeUpdateRelationships(HashSet ids, - IAffectedRelationships
resourcesByRelationship, ResourcePipeline pipeline) - { - // the entities stored in the IAffectedRelationships
instance - // are plain resource identifier objects when LoadDatabaseValues is turned off, - // or objects loaded from the database when LoadDatabaseValues is turned on. - } - } -} -``` - -Note that there are some hooks that the `LoadDatabaseValues` option and attribute does not affect. The only hooks that are affected are: -* `BeforeUpdate` -* `BeforeUpdateRelationship` -* `BeforeDelete` - - -# 3. Advanced usage - -## Simple authorization: explicitly affected resources -Resource hooks can be used to easily implement authorization in your application. As an example, consider the case in which an API user is not allowed to see anonymous people, which is reflected by the `Anonymous` property on `Person` being set to `true`. The API should handle this as follows: -* When reading people (`GET /people`), it should hide all people that are set to anonymous. -* When reading a single person (`GET /people/{id}`), it should throw an authorization error if the particular requested person is set to anonymous. - -This can be achieved as follows: - -```c# -public class PersonResource : ResourceHooksDefinition -{ - private readonly _IAuthorizationHelper _auth; - - public constructor PersonResource(IAuthorizationHelper auth) - { - // IAuthorizationHelper is a helper service that handles all authorization related logic. - _auth = auth; - } - - public override IEnumerable OnReturn(HashSet entities, - ResourcePipeline pipeline) - { - if (!_auth.CanSeeSecretPeople()) - { - if (pipeline == ResourcePipeline.GetSingle) - { - throw new JsonApiException(403, "Forbidden to view this person", - new UnauthorizedAccessException()); - } - - entities = entities.Where(person => !person.IsSecret) - } - - return entities; - } -} -``` - -This example of authorization is considered simple because it only involves one resource. The next example shows a more complex case. - -## Advanced authorization: implicitly affected resources -Let's consider an authorization scenario for which we are required to implement multiple hooks across multiple resource definitions. We will assume the following: -* There exists a one-to-one relationship between `Article` and `Person`: an article can have only one author, and a person can be author of only one article. -* The author of article `Old Article` is person `Alice`. -* The author of article `New Article` is person `Bob`. - -Now let's consider an API user that tries to update `New Article` by setting its author to `Alice`. The request would look something like `PATCH /articles/{ArticleId}` with a body containing a reference to `Alice`. - -First to all, we wish to authorize this operation by the verifying permissions related to the resources that are **explicity affected** by it: -1. Is the API user allowed to update `Article`? -2. Is the API user allowed to update `Alice`? - -Apart from this, we also wish to verify permissions for the resources that are **implicitly affected** by this operation: `Bob` and `Old Article`. Setting `Alice` as the new author of `Article` will result in removing the following two relationships: `Bob` being an author of `Article`, and `Alice` being an author of `Old Article`. Therefore, we wish wish to verify the related permissions: - -3. Is the API user allowed to update `Bob`? -4. Is the API user allowed to update `Old Article`? - -This authorization requirement can be fulfilled as follows. - -For checking the permissions for the explicitly affected resources, `Article` and `Alice`, we may implement the `BeforeUpdate` hook for `Article`: - -```c# -public override IEnumerable
BeforeUpdate(IResourceDiff
entityDiff, - ResourcePipeline pipeline) -{ - if (pipeline == ResourcePipeline.Patch) - { - Article article = entityDiff.RequestEntities.Single(); - - if (!_auth.CanEditResource(article)) - { - throw new JsonApiException(403, "Forbidden to update properties of this article", - new UnauthorizedAccessException()); - } - - if (entityDiff.GetByRelationship().Any() && - _auth.CanEditRelationship(article)) - { - throw new JsonApiException(403, "Forbidden to update relationship of this article", - new UnauthorizedAccessException()); - } - } - - return entityDiff.RequestEntities; -} -``` - -and the `BeforeUpdateRelationship` hook for `Person`: - -```c# -public override IEnumerable BeforeUpdateRelationship(HashSet ids, - IAffectedRelationships resourcesByRelationship, ResourcePipeline pipeline) -{ - var updatedOwnerships = resourcesByRelationship.GetByRelationship
(); - - if (updatedOwnerships.Any()) - { - Person person = - resourcesByRelationship.GetByRelationship
().Single().Value.First(); - - if (_auth.CanEditRelationship
(person)) - { - throw new JsonApiException(403, "Forbidden to update relationship of this person", - new UnauthorizedAccessException()); - } - } - - return ids; -} -``` - -To verify the permissions for the implicitly affected resources, `Old Article` and `Bob`, we need to implement the `BeforeImplicitUpdateRelationship` hook for `Article`: - -```c# -public override void BeforeImplicitUpdateRelationship( - IAffectedRelationships
resourcesByRelationship, ResourcePipeline pipeline) -{ - var updatedOwnerships = resourcesByRelationship.GetByRelationship(); - - if (updatedOwnerships.Any()) - { - Article article = - resourcesByRelationship.GetByRelationship().Single().Value.First(); - - if (_auth.CanEditRelationship(article)) - { - throw new JsonApiException(403, "Forbidden to update relationship of this article", - new UnauthorizedAccessException()); - } - } -} -``` - -and similarly for `Person`: - -```c# -public override void BeforeImplicitUpdateRelationship( - IAffectedRelationships resourcesByRelationship, ResourcePipeline pipeline) -{ - var updatedOwnerships = resourcesByRelationship.GetByRelationship
(); - if (updatedOwnerships.Any()) - { - Person person = - resourcesByRelationship.GetByRelationship
().Single().Value.First(); - - if (_auth.CanEditRelationship
(person)) - { - throw new JsonApiException(403, "Forbidden to update relationship of this article", - new UnauthorizedAccessException()); - } - } -} -``` - -## Using Resource Hooks without Entity Framework Core - -If you want to use Resource Hooks without Entity Framework 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: -1. The corresponding resource repository must fully implement `IResourceReadRepository` -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](#4-hook-execution-overview) to determine which hook should be fired in which scenario. - -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 : - -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: - -```c# -public class Article : Identifiable -{ - [HasOne("author", InverseNavigationProperty: "OwnerOfArticle")] - public Person Author { get; set; } -} - -public class Person : Identifiable -{ - [HasOne("article")] - public Article OwnerOfArticle { get; set; } -} -``` - -2. **Tell JsonApiDotNetCore how to do this in general**. For full support, you can provide JsonApiDotNetCore with a custom service implementation of the `IInverseNavigationResolver` interface. relationship of the example, then it is easiest to provide the `InverseNavigationProperty` as follows: - -```c# -public class CustomInverseNavigationResolver : IInverseNavigationResolver -{ - public void Resolve() - { - // the implementation of this method depends completely on - // the data access layer you're using. - // It should set the RelationshipAttribute.InverseNavigationProperty property - // for all (relevant) relationships. - // To have an idea of how to implement this method, see the InverseNavigationResolver class - // in the source code of JsonApiDotNetCore. - } -} -``` - -This service will then be run once at startup and take care of the metadata that is required for `BeforeImplicitUpdateRelationship` to be supported. - -*Note: don't forget to register this singleton service with the service provider.* - - -## Synchronizing data across microservices -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. - -```c# -public class ArticleResource : ResourceHooksDefinition
-{ - private readonly IEventBus _bus; - private readonly IJsonApiContext _context; - - public ArticleResource(IEventBus bus, IJsonApiContext context) - { - _bus = bus; - _context = context; - } - - public override void AfterCreate(HashSet
entities, ResourcePipeline pipeline) - { - foreach (var article in entities ) - { - var @event = new ResourceCreatedEvent(article); - _bus.Publish(@event); - } - } - - public override void AfterDelete(HashSet
entities, ResourcePipeline pipeline, - bool succeeded) - { - foreach (var article in entities) - { - var @event = new ResourceDeletedEvent(article); - _bus.Publish(@event); - } - } - - public override void AfterUpdate(HashSet
entities, ResourcePipeline pipeline) - { - foreach (var article in entities) - { - // You could inject ITargetedFields and use it to pass along - // only the attributes that were updated - - var @event = new ResourceUpdatedEvent(article, - properties: _targetedFields.Attributes); - - _bus.Publish(@event); - } - } -} -``` - -## Hooks for many-to-many join tables -In this example we consider an application with a many-to-many relationship: `Article` and `Tag`, with an internally used `ArticleTag` join-type. - -Usually, join table records will not contain any extra information other than that which is used internally for the many-to-many relationship. For this example, the join-type should then look like: - -```c# -public class ArticleTag -{ - public int ArticleId { get; set; } - public Article Article { get; set; } - - public int TagId { get; set; } - public Tag Tag { get; set; } -} -``` - -If we then eg. implement the `AfterRead` and `OnReturn` hook for `Article` and `Tag`, and perform a `GET /articles?include=tags` request, we may expect the following order of execution: - -1. Article AfterRead -2. Tag AfterRead -3. Article OnReturn -4. Tag OnReturn - -Note that under the hood, the *join table records* (instances of `ArticleTag`) are also being read, but we did not implement any hooks for them. In this example, for these records, there is little relevant business logic that can be thought of. - -Sometimes, however, relevant data may be stored in the join table of a many-to-many relationship. Let's imagine we wish to add a property `LinkDate` to the join table that reflects when a tag was added to an article. In this case, we may want to execute business logic related to these records: we may for example want to hide any tags that were added to an article longer than 2 weeks ago. - -In order to achieve this, we need to change `ArticleTag` to `ArticleTagWithLinkDate` as follows: - -```c# -public class ArticleTagWithLinkDate : Identifiable -{ - public int ArticleId { get; set; } - - [HasOne("Article")] - public Article Article { get; set; } - - public int TagId { get; set; } - - [HasOne("Tag")] - public Tag Tag { get; set; } - - public DateTime LinkDate { get; set; } -} -``` - -Then, we may implement a hook for `ArticleTagWithLinkDate` as usual: - -```c# -public class ArticleTagWithLinkDateResource : ResourceHooksDefinition -{ - public override IEnumerable OnReturn( - HashSet entities, ResourcePipeline pipeline) - { - return entities.Where(article => (DateTime.Now - article.LinkDate) < 14); - } -} -``` - -Then, for the same request `GET /articles?include=tags`, the order of execution of the hooks will look like: -1. Article AfterRead -2. Tag AfterRead -3. Article OnReturn -4. ArticleTagWithLinkDate OnReturn -5. Tag OnReturn - -And the included collection of tags per article will only contain tags that were added less than two weeks ago. - -Note that the introduced inheritance and added relationship attributes does not further affect the many-to-many relationship internally. - -# 4. Hook execution overview - - -This table below shows the involved hooks per pipeline. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PipelineExecution Flow
Before HooksRepository ActionsAfter HooksOnReturn
GetBeforeReadreadAfterRead[x]
GetSingleBeforeReadAfterRead[x]
GetRelationshipBeforeReadAfterRead[x]
PostBeforeCreatecreate
update relationship
AfterCreate[x]
PatchBeforeUpdate
BeforeUpdateRelationship
BeforeImplicitUpdateRelationship
update
update relationship
implicit update relationship
AfterUpdate
AfterUpdateRelationship
[x]
PatchRelationshipBeforeUpdate
BeforeUpdateRelationship
update
update relationship
implicit update relationship
AfterUpdate
AfterUpdateRelationship
[ ]
DeleteBeforeDeletedelete
implicit update relationship
AfterDelete[ ]
BulkPostNot yet supported
BulkPatchNot yet supported
BulkDeleteNot yet supported
diff --git a/src/JsonApiDotNetCore/CollectionConverter.cs b/src/JsonApiDotNetCore/CollectionConverter.cs index a79757ceac..212f2a6f3b 100644 --- a/src/JsonApiDotNetCore/CollectionConverter.cs +++ b/src/JsonApiDotNetCore/CollectionConverter.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore { - internal class CollectionConverter + internal sealed class CollectionConverter { private static readonly Type[] HashSetCompatibleCollectionTypes = { diff --git a/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs b/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs deleted file mode 100644 index 5806790fb5..0000000000 --- a/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Configuration -{ - /// - public sealed class GenericServiceFactory : IGenericServiceFactory - { - private readonly IServiceProvider _serviceProvider; - - public GenericServiceFactory(IRequestScopedServiceProvider serviceProvider) - { - ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); - - _serviceProvider = serviceProvider; - } - - /// - public TInterface Get(Type openGenericType, Type resourceType) - { - ArgumentGuard.NotNull(openGenericType, nameof(openGenericType)); - ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - - return GetInternal(openGenericType, resourceType); - } - - /// - public TInterface Get(Type openGenericType, Type resourceType, Type keyType) - { - ArgumentGuard.NotNull(openGenericType, nameof(openGenericType)); - ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - ArgumentGuard.NotNull(keyType, nameof(keyType)); - - return GetInternal(openGenericType, resourceType, keyType); - } - - private TInterface GetInternal(Type openGenericType, params Type[] types) - { - Type concreteType = openGenericType.MakeGenericType(types); - - return (TInterface)_serviceProvider.GetService(concreteType); - } - } -} diff --git a/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs b/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs deleted file mode 100644 index a730642e71..0000000000 --- a/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using JetBrains.Annotations; - -namespace JsonApiDotNetCore.Configuration -{ - /// - /// Represents the Service Locator design pattern. Used to obtain object instances for types are not known until runtime. This is only used by resource - /// hooks and subject to be removed in a future version. - /// - [PublicAPI] - public interface IGenericServiceFactory - { - /// - /// Constructs the generic type and locates the service, then casts to . - /// - /// - /// (typeof(GenericProcessor<>), typeof(TResource)); - /// ]]> - /// - TInterface Get(Type openGenericType, Type resourceType); - - /// - /// Constructs the generic type and locates the service, then casts to . - /// - /// - /// (typeof(GenericProcessor<>), typeof(TResource), typeof(TId)); - /// ]]> - /// - TInterface Get(Type openGenericType, Type resourceType, Type keyType); - } -} diff --git a/src/JsonApiDotNetCore/Configuration/IInverseNavigationResolver.cs b/src/JsonApiDotNetCore/Configuration/IInverseNavigationResolver.cs index 0a6efb3a28..4a7aac4ec2 100644 --- a/src/JsonApiDotNetCore/Configuration/IInverseNavigationResolver.cs +++ b/src/JsonApiDotNetCore/Configuration/IInverseNavigationResolver.cs @@ -5,9 +5,8 @@ namespace JsonApiDotNetCore.Configuration { /// /// Responsible for populating . This service is instantiated in the configure phase of the - /// application. When using a data access layer different from EF Core, and when using ResourceHooks that depend on the inverse navigation property - /// (BeforeImplicitUpdateRelationship), you will need to override this service, or set - /// explicitly. + /// application. When using a data access layer different from EF Core, you will need to implement and register this service, or set + /// explicitly. /// [PublicAPI] public interface IInverseNavigationResolver diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index f735e9c2ef..bee40d62c9 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -118,16 +118,6 @@ internal NamingStrategy SerializerNamingStrategy /// bool AllowClientGeneratedIds { get; } - /// - /// Whether or not resource hooks are enabled. This is currently an experimental feature and subject to change in future versions. Defaults to False. - /// - public bool EnableResourceHooks { get; } - - /// - /// Whether or not database values should be included by default for resource hooks. Ignored if EnableResourceHooks is set to false. False by default. - /// - bool LoadDatabaseValues { get; } - /// /// Whether or not to produce an error on unknown query string parameters. False by default. /// diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index eb2be47056..24fb1b33c8 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -3,11 +3,6 @@ using System.Linq; using JsonApiDotNetCore.AtomicOperations; using JsonApiDotNetCore.AtomicOperations.Processors; -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Hooks.Internal.Traversal; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Internal; @@ -153,9 +148,6 @@ public void ConfigureServiceContainer(ICollection dbContextTypes) AddQueryStringLayer(); AddOperationsLayer(); - AddResourceHooks(); - - _services.AddScoped(); _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); _services.AddScoped(); _services.AddScoped(); @@ -256,23 +248,6 @@ private void RegisterDependentService() _services.AddScoped(serviceProvider => serviceProvider.GetRequiredService()); } - private void AddResourceHooks() - { - if (_options.EnableResourceHooks) - { - _services.AddSingleton(typeof(IHooksDiscovery<>), typeof(HooksDiscovery<>)); - _services.AddScoped(typeof(IResourceHookContainer<>), typeof(ResourceHooksDefinition<>)); - _services.AddTransient(); - _services.AddTransient(); - _services.AddScoped(); - _services.AddScoped(); - } - else - { - _services.AddSingleton(); - } - } - private void AddSerializationLayer() { _services.AddScoped(); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 8214b576ed..bc9b1db2ac 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -58,12 +58,6 @@ public sealed class JsonApiOptions : IJsonApiOptions /// public bool AllowClientGeneratedIds { get; set; } - /// - public bool EnableResourceHooks { get; set; } - - /// - public bool LoadDatabaseValues { get; set; } - /// public bool AllowUnknownQueryStringParameters { get; set; } diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index a15e7c60e3..33198b051c 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using JetBrains.Annotations; -using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; @@ -134,11 +132,6 @@ private void AddInjectables(IReadOnlyCollection resourceDesc AddServices(assembly, resourceDescriptor); AddRepositories(assembly, resourceDescriptor); AddResourceDefinitions(assembly, resourceDescriptor); - - if (_options.EnableResourceHooks) - { - AddResourceHookDefinitions(assembly, resourceDescriptor); - } } } @@ -158,25 +151,6 @@ private void AddResource(ResourceDescriptor resourceDescriptor) _resourceGraphBuilder.Add(resourceDescriptor.ResourceType, resourceDescriptor.IdType); } - private void AddResourceHookDefinitions(Assembly assembly, ResourceDescriptor identifiable) - { - try - { - Type resourceDefinition = _typeLocator.GetDerivedGenericTypes(assembly, typeof(ResourceHooksDefinition<>), identifiable.ResourceType) - .SingleOrDefault(); - - if (resourceDefinition != null) - { - _services.AddScoped(typeof(ResourceHooksDefinition<>).MakeGenericType(identifiable.ResourceType), resourceDefinition); - } - } - catch (InvalidOperationException exception) - { - throw new InvalidConfigurationException($"Cannot define multiple ResourceHooksDefinition<> implementations for '{identifiable.ResourceType}'", - exception); - } - } - private void AddServices(Assembly assembly, ResourceDescriptor resourceDescriptor) { foreach (Type serviceInterface in ServiceInterfaces) diff --git a/src/JsonApiDotNetCore/Hooks/IResourceHookExecutorFacade.cs b/src/JsonApiDotNetCore/Hooks/IResourceHookExecutorFacade.cs deleted file mode 100644 index 88e812105b..0000000000 --- a/src/JsonApiDotNetCore/Hooks/IResourceHookExecutorFacade.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Hooks -{ - /// - /// Facade for execution of resource hooks. - /// - public interface IResourceHookExecutorFacade - { - void BeforeReadSingle(TId id, ResourcePipeline pipeline) - where TResource : class, IIdentifiable; - - void AfterReadSingle(TResource resource, ResourcePipeline pipeline) - where TResource : class, IIdentifiable; - - void BeforeReadMany() - where TResource : class, IIdentifiable; - - void AfterReadMany(IReadOnlyCollection resources) - where TResource : class, IIdentifiable; - - void BeforeCreate(TResource resource) - where TResource : class, IIdentifiable; - - void AfterCreate(TResource resource) - where TResource : class, IIdentifiable; - - void BeforeUpdateResource(TResource resource) - where TResource : class, IIdentifiable; - - void AfterUpdateResource(TResource resource) - where TResource : class, IIdentifiable; - - void BeforeUpdateRelationship(TResource resource) - where TResource : class, IIdentifiable; - - void AfterUpdateRelationship(TResource resource) - where TResource : class, IIdentifiable; - - void BeforeDelete(TId id) - where TResource : class, IIdentifiable; - - void AfterDelete(TId id) - where TResource : class, IIdentifiable; - - void OnReturnSingle(TResource resource, ResourcePipeline pipeline) - where TResource : class, IIdentifiable; - - IReadOnlyCollection OnReturnMany(IReadOnlyCollection resources) - where TResource : class, IIdentifiable; - - object OnReturnRelationship(object resourceOrResources); - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs deleted file mode 100644 index b707e2569f..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using JetBrains.Annotations; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using Microsoft.Extensions.DependencyInjection; - -namespace JsonApiDotNetCore.Hooks.Internal.Discovery -{ - /// - /// The default implementation for IHooksDiscovery - /// - [PublicAPI] - public class HooksDiscovery : IHooksDiscovery - where TResource : class, IIdentifiable - { - private readonly Type _boundResourceDefinitionType = typeof(ResourceHooksDefinition); - private readonly ResourceHook[] _allHooks; - - private readonly ResourceHook[] _databaseValuesAttributeAllowed = - { - ResourceHook.BeforeUpdate, - ResourceHook.BeforeUpdateRelationship, - ResourceHook.BeforeDelete - }; - - /// - public ResourceHook[] ImplementedHooks { get; private set; } - - public ResourceHook[] DatabaseValuesEnabledHooks { get; private set; } - public ResourceHook[] DatabaseValuesDisabledHooks { get; private set; } - - public HooksDiscovery(IServiceProvider provider) - { - _allHooks = Enum.GetValues(typeof(ResourceHook)).Cast().Where(hook => hook != ResourceHook.None).ToArray(); - - Type containerType; - - using (IServiceScope scope = provider.CreateScope()) - { - containerType = scope.ServiceProvider.GetService(_boundResourceDefinitionType)?.GetType(); - } - - DiscoverImplementedHooks(containerType); - } - - /// - /// Discovers the implemented hooks for a model. - /// - /// - /// The implemented hooks for model. - /// - private void DiscoverImplementedHooks(Type containerType) - { - if (containerType == null || containerType == _boundResourceDefinitionType) - { - return; - } - - var implementedHooks = new List(); - // this hook can only be used with enabled database values - List databaseValuesEnabledHooks = ResourceHook.BeforeImplicitUpdateRelationship.AsList(); - var databaseValuesDisabledHooks = new List(); - - foreach (ResourceHook hook in _allHooks) - { - MethodInfo method = containerType.GetMethod(hook.ToString("G")); - - if (method == null || method.DeclaringType == _boundResourceDefinitionType) - { - continue; - } - - implementedHooks.Add(hook); - LoadDatabaseValuesAttribute attr = method.GetCustomAttributes(true).OfType().SingleOrDefault(); - - if (attr != null) - { - if (!_databaseValuesAttributeAllowed.Contains(hook)) - { - throw new InvalidConfigurationException($"{nameof(LoadDatabaseValuesAttribute)} cannot be used on hook" + - $"{hook:G} in resource definition {containerType.Name}"); - } - - List targetList = attr.Value ? databaseValuesEnabledHooks : databaseValuesDisabledHooks; - targetList.Add(hook); - } - } - - ImplementedHooks = implementedHooks.ToArray(); - DatabaseValuesDisabledHooks = databaseValuesDisabledHooks.ToArray(); - DatabaseValuesEnabledHooks = databaseValuesEnabledHooks.ToArray(); - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs deleted file mode 100644 index c45244b491..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; - -// ReSharper disable UnusedTypeParameter - -namespace JsonApiDotNetCore.Hooks.Internal.Discovery -{ - /// - /// A singleton service for a particular TResource that stores a field of enums that represents which resource hooks have been implemented for that - /// particular resource. - /// - public interface IHooksDiscovery : IHooksDiscovery - where TResource : class, IIdentifiable - { - } - - public interface IHooksDiscovery - { - /// - /// A list of the implemented hooks for resource TResource - /// - /// - /// The implemented hooks. - /// - ResourceHook[] ImplementedHooks { get; } - - ResourceHook[] DatabaseValuesEnabledHooks { get; } - ResourceHook[] DatabaseValuesDisabledHooks { get; } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/LoadDatabaseValuesAttribute.cs b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/LoadDatabaseValuesAttribute.cs deleted file mode 100644 index 5fe3e71c39..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/LoadDatabaseValuesAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using JetBrains.Annotations; - -namespace JsonApiDotNetCore.Hooks.Internal.Discovery -{ - [PublicAPI] - [AttributeUsage(AttributeTargets.Method)] - public sealed class LoadDatabaseValuesAttribute : Attribute - { - public bool Value { get; } - - public LoadDatabaseValuesAttribute(bool value = true) - { - Value = value; - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs deleted file mode 100644 index 91e51a9660..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using JetBrains.Annotations; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - [PublicAPI] - public sealed class DiffableResourceHashSet : ResourceHashSet, IDiffableResourceHashSet - where TResource : class, IIdentifiable - { - // ReSharper disable once StaticMemberInGenericType - private static readonly CollectionConverter CollectionConverter = new CollectionConverter(); - - private readonly HashSet _databaseValues; - private readonly bool _databaseValuesLoaded; - private readonly IDictionary> _updatedAttributes; - - public DiffableResourceHashSet(HashSet requestResources, HashSet databaseResources, - IDictionary> relationships, IDictionary> updatedAttributes) - : base(requestResources, relationships) - { - _databaseValues = databaseResources; - _databaseValuesLoaded |= _databaseValues != null; - _updatedAttributes = updatedAttributes; - } - - /// - /// Used internally by the ResourceHookExecutor to make live a bit easier with generics - /// - internal DiffableResourceHashSet(IEnumerable requestResources, IEnumerable databaseResources, - IDictionary relationships, ITargetedFields targetedFields) - : this((HashSet)requestResources, (HashSet)databaseResources, - relationships.ToDictionary(pair => pair.Key, pair => (HashSet)pair.Value), - targetedFields.Attributes?.ToDictionary(attr => attr.Property, _ => (HashSet)requestResources)) - { - } - - /// - public IEnumerable> GetDiffs() - { - if (!_databaseValuesLoaded) - { - ThrowNoDbValuesError(); - } - - foreach (TResource resource in this) - { - TResource currentValueInDatabase = _databaseValues.Single(databaseResource => resource.StringId == databaseResource.StringId); - yield return new ResourceDiffPair(resource, currentValueInDatabase); - } - } - - /// - public override HashSet GetAffected(Expression> navigationAction) - { - ArgumentGuard.NotNull(navigationAction, nameof(navigationAction)); - - PropertyInfo propertyInfo = HooksNavigationParser.ParseNavigationExpression(navigationAction); - Type propertyType = propertyInfo.PropertyType; - - if (propertyType.IsOrImplementsInterface(typeof(IEnumerable))) - { - propertyType = CollectionConverter.TryGetCollectionElementType(propertyType); - } - - if (propertyType.IsOrImplementsInterface(typeof(IIdentifiable))) - { - // the navigation action references a relationship. Redirect the call to the relationship dictionary. - return base.GetAffected(navigationAction); - } - - return _updatedAttributes.TryGetValue(propertyInfo, out HashSet resources) ? resources : new HashSet(); - } - - private void ThrowNoDbValuesError() - { - throw new MemberAccessException($"Cannot iterate over the diffs if the ${nameof(LoadDatabaseValuesAttribute)} option is set to false"); - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookContainerProvider.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookContainerProvider.cs deleted file mode 100644 index e926edef72..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookContainerProvider.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Repositories; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using LeftType = System.Type; -using RightType = System.Type; - -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - /// - internal sealed class HookContainerProvider : IHookContainerProvider - { - private static readonly HooksCollectionConverter CollectionConverter = new HooksCollectionConverter(); - private static readonly HooksObjectFactory ObjectFactory = new HooksObjectFactory(); - private static readonly IncludeChainConverter IncludeChainConverter = new IncludeChainConverter(); - - private readonly IdentifiableComparer _comparer = IdentifiableComparer.Instance; - private readonly IJsonApiOptions _options; - private readonly IGenericServiceFactory _genericServiceFactory; - private readonly IResourceContextProvider _resourceContextProvider; - private readonly Dictionary _hookContainers; - private readonly Dictionary _hookDiscoveries; - private readonly List _targetedHooksForRelatedResources; - - public HookContainerProvider(IGenericServiceFactory genericServiceFactory, IResourceContextProvider resourceContextProvider, IJsonApiOptions options) - { - _options = options; - _genericServiceFactory = genericServiceFactory; - _resourceContextProvider = resourceContextProvider; - _hookContainers = new Dictionary(); - _hookDiscoveries = new Dictionary(); - _targetedHooksForRelatedResources = new List(); - } - - /// - public IResourceHookContainer GetResourceHookContainer(RightType targetResource, ResourceHook hook = ResourceHook.None) - { - // checking the cache if we have a reference for the requested container, - // regardless of the hook we will use it for. If the value is null, - // it means there was no implementation IResourceHookContainer at all, - // so we need not even bother. - if (!_hookContainers.TryGetValue(targetResource, out IResourceHookContainer container)) - { - container = _genericServiceFactory.Get(typeof(ResourceHooksDefinition<>), targetResource); - _hookContainers[targetResource] = container; - } - - if (container == null) - { - return null; - } - - // if there was a container, first check if it implements the hook we - // want to use it for. - IEnumerable targetHooks; - - if (hook == ResourceHook.None) - { - CheckForTargetHookExistence(); - targetHooks = _targetedHooksForRelatedResources; - } - else - { - targetHooks = hook.AsEnumerable(); - } - - foreach (ResourceHook targetHook in targetHooks) - { - if (ShouldExecuteHook(targetResource, targetHook)) - { - return container; - } - } - - return null; - } - - /// - public IResourceHookContainer GetResourceHookContainer(ResourceHook hook = ResourceHook.None) - where TResource : class, IIdentifiable - { - return (IResourceHookContainer)GetResourceHookContainer(typeof(TResource), hook); - } - - public IEnumerable LoadDbValues(LeftType resourceTypeForRepository, IEnumerable resources, params RelationshipAttribute[] relationshipsToNextLayer) - { - LeftType idType = ObjectFactory.GetIdType(resourceTypeForRepository); - - MethodInfo parameterizedGetWhere = - GetType().GetMethod(nameof(GetWhereWithInclude), BindingFlags.NonPublic | BindingFlags.Instance)!.MakeGenericMethod(resourceTypeForRepository, - idType); - - IEnumerable resourceIds = ((IEnumerable)resources).Cast().Select(resource => resource.GetTypedId()); - IList idsAsList = CollectionConverter.CopyToList(resourceIds, idType); - var values = (IEnumerable)parameterizedGetWhere.Invoke(this, ArrayFactory.Create(idsAsList, relationshipsToNextLayer)); - - return values == null ? null : CollectionConverter.CopyToHashSet(values, resourceTypeForRepository); - } - - public bool ShouldLoadDbValues(LeftType resourceType, ResourceHook hook) - { - IHooksDiscovery discovery = GetHookDiscovery(resourceType); - - if (discovery.DatabaseValuesDisabledHooks.Contains(hook)) - { - return false; - } - - if (discovery.DatabaseValuesEnabledHooks.Contains(hook)) - { - return true; - } - - return _options.LoadDatabaseValues; - } - - private bool ShouldExecuteHook(RightType resourceType, ResourceHook hook) - { - IHooksDiscovery discovery = GetHookDiscovery(resourceType); - return discovery.ImplementedHooks.Contains(hook); - } - - private void CheckForTargetHookExistence() - { - if (!_targetedHooksForRelatedResources.Any()) - { - throw new InvalidOperationException("Something is not right in the breadth first traversal of resource hook: " + - "trying to get meta information when no allowed hooks are set"); - } - } - - private IHooksDiscovery GetHookDiscovery(LeftType resourceType) - { - if (!_hookDiscoveries.TryGetValue(resourceType, out IHooksDiscovery discovery)) - { - discovery = _genericServiceFactory.Get(typeof(IHooksDiscovery<>), resourceType); - _hookDiscoveries[resourceType] = discovery; - } - - return discovery; - } - - private IEnumerable GetWhereWithInclude(IReadOnlyCollection ids, RelationshipAttribute[] relationshipsToNextLayer) - where TResource : class, IIdentifiable - { - if (!ids.Any()) - { - return Array.Empty(); - } - - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(); - FilterExpression filterExpression = CreateFilterByIds(ids, resourceContext); - - var queryLayer = new QueryLayer(resourceContext) - { - Filter = filterExpression - }; - - List chains = relationshipsToNextLayer.Select(relationship => new ResourceFieldChainExpression(relationship)) - .ToList(); - - if (chains.Any()) - { - queryLayer.Include = IncludeChainConverter.FromRelationshipChains(chains); - } - - IResourceReadRepository repository = GetRepository(); - return repository.GetAsync(queryLayer, CancellationToken.None).Result; - } - - private static FilterExpression CreateFilterByIds(IReadOnlyCollection ids, ResourceContext resourceContext) - { - AttrAttribute idAttribute = resourceContext.Attributes.Single(attr => attr.Property.Name == nameof(Identifiable.Id)); - var idChain = new ResourceFieldChainExpression(idAttribute); - - if (ids.Count == 1) - { - var constant = new LiteralConstantExpression(ids.Single().ToString()); - return new ComparisonExpression(ComparisonOperator.Equals, idChain, constant); - } - - List constants = ids.Select(id => new LiteralConstantExpression(id.ToString())).ToList(); - return new EqualsAnyOfExpression(idChain, constants); - } - - private IResourceReadRepository GetRepository() - where TResource : class, IIdentifiable - { - return _genericServiceFactory.Get>(typeof(IResourceReadRepository<,>), typeof(TResource), typeof(TId)); - } - - public IDictionary LoadImplicitlyAffected(IDictionary leftResourcesByRelation, - IEnumerable existingRightResources = null) - { - List existingRightResourceList = existingRightResources?.Cast().ToList(); - - var implicitlyAffected = new Dictionary(); - - foreach (KeyValuePair pair in leftResourcesByRelation) - { - RelationshipAttribute relationship = pair.Key; - IEnumerable lefts = pair.Value; - - if (relationship is HasManyThroughAttribute) - { - continue; - } - - // note that we don't have to check if BeforeImplicitUpdate hook is implemented. If not, it wont ever get here. - IEnumerable includedLefts = LoadDbValues(relationship.LeftType, lefts, relationship); - - AddToImplicitlyAffected(includedLefts, relationship, existingRightResourceList, implicitlyAffected); - } - - return implicitlyAffected.ToDictionary(pair => pair.Key, pair => CollectionConverter.CopyToHashSet(pair.Value, pair.Key.RightType)); - } - - private void AddToImplicitlyAffected(IEnumerable includedLefts, RelationshipAttribute relationship, List existingRightResourceList, - Dictionary implicitlyAffected) - { - foreach (IIdentifiable ip in includedLefts) - { - object relationshipValue = relationship.GetValue(ip); - ICollection dbRightResources = CollectionConverter.ExtractResources(relationshipValue); - - if (existingRightResourceList != null) - { - dbRightResources = dbRightResources.Except(existingRightResourceList, _comparer).ToList(); - } - - if (dbRightResources.Any()) - { - if (!implicitlyAffected.TryGetValue(relationship, out IEnumerable affected)) - { - affected = CollectionConverter.CopyToList(Array.Empty(), relationship.RightType); - implicitlyAffected[relationship] = affected; - } - - AddToList((IList)affected, dbRightResources); - } - } - } - - private static void AddToList(IList list, IEnumerable itemsToAdd) - { - foreach (object item in itemsToAdd) - { - list.Add(item); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IByAffectedRelationships.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IByAffectedRelationships.cs deleted file mode 100644 index a70b643675..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IByAffectedRelationships.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - /// - /// An interface that is implemented to expose a relationship dictionary on another class. - /// - [PublicAPI] - public interface IByAffectedRelationships : IRelationshipGetters - where TRightResource : class, IIdentifiable - { - /// - /// Gets a dictionary of affected resources grouped by affected relationships. - /// - IDictionary> AffectedRelationships { get; } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IDiffableResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IDiffableResourceHashSet.cs deleted file mode 100644 index ab712a0bc6..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IDiffableResourceHashSet.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - /// - /// A wrapper class that contains information about the resources that are updated by the request. Contains the resources from the request and the - /// corresponding database values. Also contains information about updated relationships through implementation of IRelationshipsDictionary - /// > - /// - public interface IDiffableResourceHashSet : IResourceHashSet - where TResource : class, IIdentifiable - { - /// - /// Iterates over diffs, which is the affected resource from the request with their associated current value from the database. - /// - IEnumerable> GetDiffs(); - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookContainerProvider.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookContainerProvider.cs deleted file mode 100644 index bde89d870a..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookContainerProvider.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - /// - /// A helper class for retrieving meta data about hooks, fetching database values and performing other recurring internal operations. Used internally by - /// - /// - internal interface IHookContainerProvider - { - /// - /// For a particular ResourceHook and for a given model type, checks if the ResourceHooksDefinition has an implementation for the hook and if so, return - /// it. Also caches the retrieves containers so we don't need to reflectively instantiate them multiple times. - /// - IResourceHookContainer GetResourceHookContainer(Type targetResource, ResourceHook hook = ResourceHook.None); - - /// - /// For a particular ResourceHook and for a given model type, checks if the ResourceHooksDefinition has an implementation for the hook and if so, return - /// it. Also caches the retrieves containers so we don't need to reflectively instantiate them multiple times. - /// - IResourceHookContainer GetResourceHookContainer(ResourceHook hook = ResourceHook.None) - where TResource : class, IIdentifiable; - - /// - /// Load the implicitly affected resources from the database for a given set of target target resources and involved relationships - /// - /// - /// The implicitly affected resources by relationship - /// - IDictionary LoadImplicitlyAffected(IDictionary leftResourcesByRelation, - IEnumerable existingRightResources = null); - - /// - /// For a set of resources, loads current values from the database - /// - /// - /// type of the resources to be loaded - /// - /// - /// The set of resources to load the db values for - /// - /// - /// Relationships that need to be included on resources. - /// - IEnumerable LoadDbValues(Type resourceTypeForRepository, IEnumerable resources, params RelationshipAttribute[] relationships); - - /// - /// Checks if the display database values option is allowed for the targeted hook, and for a given resource of type - /// checks if this hook is implemented and if the database values option is enabled. - /// - /// - /// true, if should load db values, false otherwise. - /// - /// - /// Container resource type. - /// - /// Hook. - bool ShouldLoadDbValues(Type resourceType, ResourceHook hook); - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs deleted file mode 100644 index 37e300151c..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - /// - /// A helper class that provides insights in which relationships have been updated for which resources. - /// - [PublicAPI] - public interface IRelationshipGetters - where TLeftResource : class, IIdentifiable - { - /// - /// Gets a dictionary of all resources that have an affected relationship to type - /// - IDictionary> GetByRelationship() - where TRightResource : class, IIdentifiable; - - /// - /// Gets a dictionary of all resources that have an affected relationship to type - /// - IDictionary> GetByRelationship(Type resourceType); - - /// - /// Gets a collection of all the resources for the property within has been affected by the request - /// -#pragma warning disable AV1130 // Return type in method signature should be a collection interface instead of a concrete type - HashSet GetAffected(Expression> navigationAction); -#pragma warning restore AV1130 // Return type in method signature should be a collection interface instead of a concrete type - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs deleted file mode 100644 index 6f1496b01d..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - /// - /// A dummy interface used internally by the hook executor. - /// - public interface IRelationshipsDictionary - { - } - - /// - /// A helper class that provides insights in which relationships have been updated for which resources. - /// - public interface IRelationshipsDictionary - : IRelationshipGetters, IReadOnlyDictionary>, IRelationshipsDictionary - where TRightResource : class, IIdentifiable - { - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IResourceHashSet.cs deleted file mode 100644 index 045880be3f..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IResourceHashSet.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - /// - /// Basically a enumerable of of resources that were affected by the request. Also contains information about updated - /// relationships through implementation of IAffectedRelationshipsDictionary> - /// - public interface IResourceHashSet : IByAffectedRelationships, IReadOnlyCollection - where TResource : class, IIdentifiable - { - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs deleted file mode 100644 index 140b0cdd07..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - /// - /// Implementation of IAffectedRelationships{TRightResource} It is practically a ReadOnlyDictionary{RelationshipAttribute, HashSet{TRightResource}} - /// dictionary with the two helper methods defined on IAffectedRelationships{TRightResource}. - /// - [PublicAPI] - public class RelationshipsDictionary : Dictionary>, IRelationshipsDictionary - where TResource : class, IIdentifiable - { - /// - /// Initializes a new instance of the class. - /// - /// - /// Relationships. - /// - public RelationshipsDictionary(IDictionary> relationships) - : base(relationships) - { - } - - /// - /// Used internally by the ResourceHookExecutor to make life a bit easier with generics - /// - internal RelationshipsDictionary(IDictionary relationships) - : this(relationships.ToDictionary(pair => pair.Key, pair => (HashSet)pair.Value)) - { - } - - /// - public IDictionary> GetByRelationship() - where TRelatedResource : class, IIdentifiable - { - return GetByRelationship(typeof(TRelatedResource)); - } - - /// - public IDictionary> GetByRelationship(Type resourceType) - { - return this.Where(pair => pair.Key.RightType == resourceType).ToDictionary(pair => pair.Key, pair => pair.Value); - } - - /// - public HashSet GetAffected(Expression> navigationAction) - { - ArgumentGuard.NotNull(navigationAction, nameof(navigationAction)); - - PropertyInfo property = HooksNavigationParser.ParseNavigationExpression(navigationAction); - return this.Where(pair => pair.Key.Property.Name == property.Name).Select(pair => pair.Value).SingleOrDefault(); - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs deleted file mode 100644 index c338cbe612..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs +++ /dev/null @@ -1,29 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - /// - /// A wrapper that contains a resource that is affected by the request, matched to its current database value - /// - [PublicAPI] - public sealed class ResourceDiffPair - where TResource : class, IIdentifiable - { - /// - /// The resource from the request matching the resource from the database. - /// - public TResource Resource { get; } - - /// - /// The resource from the database matching the resource from the request. - /// - public TResource DatabaseValue { get; } - - public ResourceDiffPair(TResource resource, TResource databaseValue) - { - Resource = resource; - DatabaseValue = databaseValue; - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs deleted file mode 100644 index ac6c24bf51..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - /// - /// Implementation of IResourceHashSet{TResource}. Basically a enumerable of of resources that were affected by the - /// request. Also contains information about updated relationships through implementation of IRelationshipsDictionary> - /// - [PublicAPI] - public class ResourceHashSet : HashSet, IResourceHashSet - where TResource : class, IIdentifiable - { - private readonly RelationshipsDictionary _relationships; - - /// - public IDictionary> AffectedRelationships => _relationships; - - public ResourceHashSet(HashSet resources, IDictionary> relationships) - : base(resources) - { - _relationships = new RelationshipsDictionary(relationships); - } - - /// - /// Used internally by the ResourceHookExecutor to make live a bit easier with generics - /// - internal ResourceHashSet(IEnumerable resources, IDictionary relationships) - : this((HashSet)resources, relationships.ToDictionary(pair => pair.Key, pair => (HashSet)pair.Value)) - { - } - - /// - public IDictionary> GetByRelationship(Type resourceType) - { - return _relationships.GetByRelationship(resourceType); - } - - /// - public IDictionary> GetByRelationship() - where TRightResource : class, IIdentifiable - { - return GetByRelationship(typeof(TRightResource)); - } - - /// - public virtual HashSet GetAffected(Expression> navigationAction) - { - return _relationships.GetAffected(navigationAction); - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHook.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHook.cs deleted file mode 100644 index f63b1380ad..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHook.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - /// - /// A enum that represent the available resource hooks. - /// - public enum ResourceHook - { - None, // https://stackoverflow.com/questions/24151354/is-it-a-good-practice-to-add-a-null-or-none-member-to-the-enum - BeforeCreate, - BeforeRead, - BeforeUpdate, - BeforeDelete, - BeforeUpdateRelationship, - BeforeImplicitUpdateRelationship, - OnReturn, - AfterCreate, - AfterRead, - AfterUpdate, - AfterDelete, - AfterUpdateRelationship - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs deleted file mode 100644 index bdfc01aa78..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Threading; -using JetBrains.Annotations; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Hooks.Internal.Execution -{ - /// - /// An enum that represents the initiator of a resource hook. Eg, when BeforeCreate() is called from - /// , it will be called with parameter pipeline = - /// ResourceAction.GetSingle. - /// - [PublicAPI] - public enum ResourcePipeline - { - None, - Get, - GetSingle, - GetRelationship, - Post, - Patch, - PatchRelationship, - Delete - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/HooksCollectionConverter.cs b/src/JsonApiDotNetCore/Hooks/Internal/HooksCollectionConverter.cs deleted file mode 100644 index 8ccf97ad21..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/HooksCollectionConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - internal sealed class HooksCollectionConverter : CollectionConverter - { - public IList CopyToList(IEnumerable elements, Type elementType) - { - Type collectionType = typeof(List<>).MakeGenericType(elementType); - return (IList)CopyToTypedCollection(elements, collectionType); - } - - public IEnumerable CopyToHashSet(IEnumerable elements, Type elementType) - { - Type collectionType = typeof(HashSet<>).MakeGenericType(elementType); - return CopyToTypedCollection(elements, collectionType); - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/HooksNavigationParser.cs b/src/JsonApiDotNetCore/Hooks/Internal/HooksNavigationParser.cs deleted file mode 100644 index 30f4c34e1c..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/HooksNavigationParser.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Linq.Expressions; -using System.Reflection; - -#pragma warning disable AV1008 // Class should not be static - -namespace JsonApiDotNetCore.Hooks.Internal -{ - internal static class HooksNavigationParser - { - /// - /// Gets the property info that is referenced in the NavigationAction expression. Credits: https://stackoverflow.com/a/17116267/4441216 - /// - public static PropertyInfo ParseNavigationExpression(Expression> navigationExpression) - { - ArgumentGuard.NotNull(navigationExpression, nameof(navigationExpression)); - - MemberExpression exp; - - // this line is necessary, because sometimes the expression comes in as Convert(originalExpression) - if (navigationExpression.Body is UnaryExpression unaryExpression) - { - if (unaryExpression.Operand is MemberExpression memberExpression) - { - exp = memberExpression; - } - else - { - throw new ArgumentException("Invalid expression.", nameof(navigationExpression)); - } - } - else if (navigationExpression.Body is MemberExpression memberExpression) - { - exp = memberExpression; - } - else - { - throw new ArgumentException("Invalid expression.", nameof(navigationExpression)); - } - - return (PropertyInfo)exp.Member; - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/HooksObjectFactory.cs b/src/JsonApiDotNetCore/Hooks/Internal/HooksObjectFactory.cs deleted file mode 100644 index d333d00514..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/HooksObjectFactory.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Reflection; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - internal sealed class HooksObjectFactory - { - /// - /// Creates an instance of the specified generic type - /// - /// - /// The instance of the parameterized generic type - /// - /// - /// Generic type parameter to be used in open type. - /// - /// - /// Constructor arguments to be provided in instantiation. - /// - /// - /// Open generic type - /// - public object CreateInstanceOfOpenType(Type openType, Type parameter, params object[] constructorArguments) - { - return CreateInstanceOfOpenType(openType, parameter.AsArray(), constructorArguments); - } - - /// - /// Use this overload if you need to instantiate a type that has an internal constructor - /// - public object CreateInstanceOfInternalOpenType(Type openType, Type parameter, params object[] constructorArguments) - { - Type[] parameters = - { - parameter - }; - - Type closedType = openType.MakeGenericType(parameters); - return Activator.CreateInstance(closedType, BindingFlags.NonPublic | BindingFlags.Instance, null, constructorArguments, null); - } - - /// - /// Creates an instance of the specified generic type - /// - /// - /// The instance of the parameterized generic type - /// - /// - /// Generic type parameters to be used in open type. - /// - /// - /// Constructor arguments to be provided in instantiation. - /// - /// - /// Open generic type - /// - private object CreateInstanceOfOpenType(Type openType, Type[] parameters, params object[] constructorArguments) - { - Type closedType = openType.MakeGenericType(parameters); - return Activator.CreateInstance(closedType, constructorArguments); - } - - /// - /// Gets the type (such as Guid or int) of the Id property on a type that implements . - /// - public Type GetIdType(Type resourceType) - { - PropertyInfo property = resourceType.GetProperty(nameof(Identifiable.Id)); - - if (property == null) - { - throw new ArgumentException($"Type '{resourceType.Name}' does not have 'Id' property."); - } - - return property.PropertyType; - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs deleted file mode 100644 index a52e9a716c..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - /// Create hooks container - /// - public interface ICreateHookContainer - where TResource : class, IIdentifiable - { - /// - /// Implement this hook to run custom logic in the layer just before creation of resources of type - /// . - /// - /// For the pipeline, will typically contain one entry. - /// - /// The returned may be a subset of , in which case the operation of the pipeline will - /// not be executed for the omitted resources. The returned set may also contain custom changes of the properties on the resources. - /// - /// If new relationships are to be created with the to-be-created resources, this will be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the hook is fired after the execution of - /// this hook. - /// - /// - /// The transformed resource set - /// - /// - /// The unique set of affected resources. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - IEnumerable BeforeCreate(IResourceHashSet resources, ResourcePipeline pipeline); - - /// - /// Implement this hook to run custom logic in the layer just after creation of resources of type - /// . - /// - /// If relationships were created with the created resources, this will be reflected by the corresponding NavigationProperty being set. For each of these - /// relationships, the - /// hook is fired after the execution of this hook. - /// - /// - /// The transformed resource set - /// - /// - /// The unique set of affected resources. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - void AfterCreate(HashSet resources, ResourcePipeline pipeline); - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs deleted file mode 100644 index c26c5099d8..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - public interface ICreateHookExecutor - { - /// - /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. The returned set will be used in the actual operation in - /// . - /// - /// Fires the hook for values in parameter . - /// - /// Fires the hook for any secondary (nested) resource for values within - /// parameter - /// - /// - /// The transformed set - /// - /// - /// Target resources for the Before cycle. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// The type of the root resources - /// - IEnumerable BeforeCreate(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable; - - /// - /// Executes the After Cycle by firing the appropriate hooks if they are implemented. - /// - /// Fires the hook for values in parameter . - /// - /// Fires the hook for any secondary (nested) resource for values within - /// parameter - /// - /// - /// Target resources for the Before cycle. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// The type of the root resources - /// - void AfterCreate(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable; - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs deleted file mode 100644 index 595056ce07..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - /// Delete hooks container - /// - public interface IDeleteHookContainer - where TResource : class, IIdentifiable - { - /// - /// Implement this hook to run custom logic in the layer just before deleting resources of type - /// . - /// - /// For the pipeline, will typically contain one resource. - /// - /// The returned may be a subset of , in which case the operation of the pipeline will - /// not be executed for the omitted resources. - /// - /// If by the deletion of these resources any other resources are affected implicitly by the removal of their relationships (eg in the case of an - /// one-to-one relationship), the hook is fired for these resources. - /// - /// - /// The transformed resource set - /// - /// - /// The unique set of affected resources. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline); - - /// - /// Implement this hook to run custom logic in the layer just after deletion of resources of type - /// . - /// - /// - /// The unique set of affected resources. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// If set to true the deletion succeeded in the repository layer. - /// - void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded); - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs deleted file mode 100644 index 706477ad42..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - public interface IDeleteHookExecutor - { - /// - /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. The returned set will be used in the actual operation in - /// . - /// - /// Fires the hook for values in parameter . - /// - /// Fires the hook for any resources that are indirectly (implicitly) - /// affected by this operation. Eg: when deleting a resource that has relationships set to other resources, these other resources are implicitly affected - /// by the delete operation. - /// - /// - /// The transformed set - /// - /// - /// Target resources for the Before cycle. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// The type of the root resources - /// - IEnumerable BeforeDelete(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable; - - /// - /// Executes the After Cycle by firing the appropriate hooks if they are implemented. - /// - /// Fires the hook for values in parameter . - /// - /// - /// Target resources for the Before cycle. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// If set to true the deletion succeeded. - /// - /// - /// The type of the root resources - /// - void AfterDelete(IEnumerable resources, ResourcePipeline pipeline, bool succeeded) - where TResource : class, IIdentifiable; - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookContainer.cs deleted file mode 100644 index 94b60ead58..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookContainer.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - /// On return hook container - /// - public interface IOnReturnHookContainer - where TResource : class, IIdentifiable - { - /// - /// Implement this hook to transform the result data just before returning the resources of type from the - /// layer - /// - /// The returned may be a subset of and may contain changes in properties of the - /// encapsulated resources. - /// - /// - /// - /// The transformed resource set - /// - /// - /// The unique set of affected resources - /// - /// - /// An enum indicating from where the hook was triggered. - /// - IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline); - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs deleted file mode 100644 index 9dca66a068..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - /// Wrapper interface for all On execution methods. - /// - public interface IOnReturnHookExecutor - { - /// - /// Executes the On Cycle by firing the appropriate hooks if they are implemented. - /// - /// Fires the for every unique resource type occurring in parameter - /// . - /// - /// - /// The transformed set - /// - /// - /// Target resources for the Before cycle. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// The type of the root resources - /// - IEnumerable OnReturn(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable; - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IReadHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IReadHookContainer.cs deleted file mode 100644 index d7ad9117a6..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/IReadHookContainer.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - /// Read hooks container - /// - public interface IReadHookContainer - where TResource : class, IIdentifiable - { - /// - /// Implement this hook to run custom logic in the layer just before reading resources of type - /// . - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// Indicates whether the to be queried resources are the primary request resources or if they were included - /// - /// - /// The string ID of the requested resource, in the case of - /// - void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null); - - /// - /// Implement this hook to run custom logic in the layer just after reading resources of type - /// . - /// - /// - /// The unique set of affected resources. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// A boolean to indicate whether the resources in this hook execution are the primary resources of the request, or if they were included as a - /// relationship - /// - void AfterRead(HashSet resources, ResourcePipeline pipeline, bool isIncluded = false); - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs deleted file mode 100644 index a752d81e2d..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - /// Wrapper interface for all Before execution methods. - /// - public interface IReadHookExecutor - { - /// - /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. - /// - /// Fires the hook for the requested resources as well as any related relationship. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// StringId of the requested resource in the case of . - /// - /// - /// The type of the request resource - /// - void BeforeRead(ResourcePipeline pipeline, string stringId = null) - where TResource : class, IIdentifiable; - - /// - /// Executes the After Cycle by firing the appropriate hooks if they are implemented. - /// - /// Fires the for every unique resource type occurring in parameter - /// . - /// - /// - /// Target resources for the Before cycle. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// The type of the root resources - /// - void AfterRead(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable; - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs deleted file mode 100644 index f3bbc0697b..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs +++ /dev/null @@ -1,21 +0,0 @@ -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - /// Not meant for public usage. Used internally in the - /// - public interface IResourceHookContainer - { - } - - /// - /// Implement this interface to implement business logic hooks on . - /// - public interface IResourceHookContainer - : IReadHookContainer, IDeleteHookContainer, ICreateHookContainer, IUpdateHookContainer, - IOnReturnHookContainer, IResourceHookContainer - where TResource : class, IIdentifiable - { - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs deleted file mode 100644 index b3baceaf88..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Hooks.Internal.Traversal; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - /// Transient service responsible for executing Resource Hooks as defined in . See methods in - /// , and for more information. Uses - /// for traversal of nested resource data structures. Uses for retrieving meta data - /// about hooks, fetching database values and performing other recurring internal operations. - /// - public interface IResourceHookExecutor : IReadHookExecutor, IUpdateHookExecutor, ICreateHookExecutor, IDeleteHookExecutor, IOnReturnHookExecutor - { - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs deleted file mode 100644 index bcae4b5f9b..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - /// update hooks container - /// - public interface IUpdateHookContainer - where TResource : class, IIdentifiable - { - /// - /// Implement this hook to run custom logic in the layer just before updating resources of type - /// . - /// - /// For the pipeline, the will typically contain one resource. - /// - /// The returned may be a subset of the property in parameter - /// , in which case the operation of the pipeline will not be executed for the omitted resources. The returned set may also - /// contain custom changes of the properties on the resources. - /// - /// If new relationships are to be created with the to-be-updated resources, this will be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the hook is fired after the execution of - /// this hook. - /// - /// If by the creation of these relationships, any other relationships (eg in the case of an already populated one-to-one relationship) are implicitly - /// affected, the hook is fired for these. - /// - /// - /// The transformed resource set - /// - /// - /// The affected resources. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - IEnumerable BeforeUpdate(IDiffableResourceHashSet resources, ResourcePipeline pipeline); - - /// - /// Implement this hook to run custom logic in the layer just before updating relationships to - /// resources of type . - /// - /// This hook is fired when a relationship is created to resources of type from a dependent pipeline ( - /// or ). For example, If an Article was created and its author relationship - /// was set to an existing Person, this hook will be fired for that particular Person. - /// - /// The returned may be a subset of , in which case the operation of the pipeline will not - /// be executed for any resource whose ID was omitted - /// - /// - /// - /// The transformed set of ids - /// - /// - /// The unique set of ids - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// A helper that groups the resources by the affected relationship - /// - IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, - ResourcePipeline pipeline); - - /// - /// Implement this hook to run custom logic in the layer just after updating resources of type - /// . - /// - /// If relationships were updated with the updated resources, this will be reflected by the corresponding NavigationProperty being set. For each of these - /// relationships, the - /// hook is fired after the execution of this hook. - /// - /// - /// The unique set of affected resources. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - void AfterUpdate(HashSet resources, ResourcePipeline pipeline); - - /// - /// Implement this hook to run custom logic in the layer just after a relationship was updated. - /// - /// - /// Relationship helper. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - void AfterUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline); - - /// - /// Implement this hook to run custom logic in the layer just before implicitly updating relationships - /// to resources of type . - /// - /// This hook is fired when a relationship to resources of type is implicitly affected from a dependent pipeline ( - /// or ). For example, if an Article was updated by setting its author - /// relationship (one-to-one) to an existing Person, and by this the relationship to a different Person was implicitly removed, this hook will be fired - /// for the latter Person. - /// - /// See for information about when - /// this hook is fired. - /// - /// - /// - /// The transformed set of ids - /// - /// - /// A helper that groups the resources by the affected relationship - /// - /// - /// An enum indicating from where the hook was triggered. - /// - void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline); - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs deleted file mode 100644 index 67323ca1ea..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - /// Wrapper interface for all After execution methods. - /// - public interface IUpdateHookExecutor - { - /// - /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. The returned set will be used in the actual operation in - /// . - /// - /// Fires the hook for values in - /// parameter . - /// - /// Fires the hook for any secondary (nested) resource for values within - /// parameter - /// - /// Fires the hook for any resources that are indirectly (implicitly) - /// affected by this operation. Eg: when updating a one-to-one relationship of a resource which already had this relationship populated, then this update - /// will indirectly affect the existing relationship value. - /// - /// - /// The transformed set - /// - /// - /// Target resources for the Before cycle. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// The type of the root resources - /// - IEnumerable BeforeUpdate(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable; - - /// - /// Executes the After Cycle by firing the appropriate hooks if they are implemented. - /// - /// Fires the hook for values in parameter . - /// - /// Fires the hook for any secondary (nested) resource for values within - /// parameter - /// - /// - /// Target resources for the Before cycle. - /// - /// - /// An enum indicating from where the hook was triggered. - /// - /// - /// The type of the root resources - /// - void AfterUpdate(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable; - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/NeverResourceHookExecutorFacade.cs b/src/JsonApiDotNetCore/Hooks/Internal/NeverResourceHookExecutorFacade.cs deleted file mode 100644 index aabff919c1..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/NeverResourceHookExecutorFacade.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - /// Facade for hooks that never executes any callbacks, which is used when is false. - /// - public sealed class NeverResourceHookExecutorFacade : IResourceHookExecutorFacade - { - public void BeforeReadSingle(TId id, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - } - - public void AfterReadSingle(TResource resource, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - } - - public void BeforeReadMany() - where TResource : class, IIdentifiable - { - } - - public void AfterReadMany(IReadOnlyCollection resources) - where TResource : class, IIdentifiable - { - } - - public void BeforeCreate(TResource resource) - where TResource : class, IIdentifiable - { - } - - public void AfterCreate(TResource resource) - where TResource : class, IIdentifiable - { - } - - public void BeforeUpdateResource(TResource resource) - where TResource : class, IIdentifiable - { - } - - public void AfterUpdateResource(TResource resource) - where TResource : class, IIdentifiable - { - } - - public void BeforeUpdateRelationship(TResource resource) - where TResource : class, IIdentifiable - { - } - - public void AfterUpdateRelationship(TResource resource) - where TResource : class, IIdentifiable - { - } - - public void BeforeDelete(TId id) - where TResource : class, IIdentifiable - { - } - - public void AfterDelete(TId id) - where TResource : class, IIdentifiable - { - } - - public void OnReturnSingle(TResource resource, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - } - - public IReadOnlyCollection OnReturnMany(IReadOnlyCollection resources) - where TResource : class, IIdentifiable - { - return resources; - } - - public object OnReturnRelationship(object resourceOrResources) - { - return resourceOrResources; - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs deleted file mode 100644 index e8f51e551d..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs +++ /dev/null @@ -1,623 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Hooks.Internal.Traversal; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using LeftType = System.Type; -using RightType = System.Type; - -// ReSharper disable PossibleMultipleEnumeration - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - internal sealed class ResourceHookExecutor : IResourceHookExecutor - { - private static readonly IncludeChainConverter IncludeChainConverter = new IncludeChainConverter(); - private static readonly HooksObjectFactory ObjectFactory = new HooksObjectFactory(); - private static readonly HooksCollectionConverter CollectionConverter = new HooksCollectionConverter(); - - private readonly IHookContainerProvider _containerProvider; - private readonly INodeNavigator _nodeNavigator; - private readonly IEnumerable _constraintProviders; - private readonly ITargetedFields _targetedFields; - private readonly IResourceGraph _resourceGraph; - - public ResourceHookExecutor(IHookContainerProvider containerProvider, INodeNavigator nodeNavigator, ITargetedFields targetedFields, - IEnumerable constraintProviders, IResourceGraph resourceGraph) - { - _containerProvider = containerProvider; - _nodeNavigator = nodeNavigator; - _targetedFields = targetedFields; - _constraintProviders = constraintProviders; - _resourceGraph = resourceGraph; - } - - /// - public void BeforeRead(ResourcePipeline pipeline, string stringId = null) - where TResource : class, IIdentifiable - { - IResourceHookContainer hookContainer = _containerProvider.GetResourceHookContainer(ResourceHook.BeforeRead); - hookContainer?.BeforeRead(pipeline, false, stringId); - List calledContainers = typeof(TResource).AsList(); - - // @formatter:wrap_chained_method_calls chop_always - // @formatter:keep_existing_linebreaks true - - IncludeExpression[] includes = _constraintProviders - .SelectMany(provider => provider.GetConstraints()) - .Select(expressionInScope => expressionInScope.Expression) - .OfType() - .ToArray(); - - // @formatter:keep_existing_linebreaks restore - // @formatter:wrap_chained_method_calls restore - - foreach (ResourceFieldChainExpression chain in includes.SelectMany(IncludeChainConverter.GetRelationshipChains)) - { - RecursiveBeforeRead(chain.Fields.Cast().ToList(), pipeline, calledContainers); - } - } - - /// - public IEnumerable BeforeUpdate(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - GetHookResult result = GetHook(ResourceHook.BeforeUpdate, resources); - - if (result.Succeeded) - { - RelationshipAttribute[] relationships = result.Node.RelationshipsToNextLayer.Select(proxy => proxy.Attribute).ToArray(); - - IEnumerable dbValues = LoadDbValues(typeof(TResource), (IEnumerable)result.Node.UniqueResources, ResourceHook.BeforeUpdate, - relationships); - - var diff = new DiffableResourceHashSet(result.Node.UniqueResources, dbValues, result.Node.LeftsToNextLayer(), _targetedFields); - IEnumerable updated = result.Container.BeforeUpdate(diff, pipeline); - result.Node.UpdateUnique(updated); - result.Node.Reassign(resources); - } - - FireNestedBeforeUpdateHooks(pipeline, _nodeNavigator.CreateNextLayer(result.Node)); - return resources; - } - - /// - public IEnumerable BeforeCreate(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - GetHookResult result = GetHook(ResourceHook.BeforeCreate, resources); - - if (result.Succeeded) - { - var affected = new ResourceHashSet((HashSet)result.Node.UniqueResources, result.Node.LeftsToNextLayer()); - IEnumerable updated = result.Container.BeforeCreate(affected, pipeline); - result.Node.UpdateUnique(updated); - result.Node.Reassign(resources); - } - - FireNestedBeforeUpdateHooks(pipeline, _nodeNavigator.CreateNextLayer(result.Node)); - return resources; - } - - /// - public IEnumerable BeforeDelete(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - GetHookResult result = GetHook(ResourceHook.BeforeDelete, resources); - - if (result.Succeeded) - { - RelationshipAttribute[] relationships = result.Node.RelationshipsToNextLayer.Select(proxy => proxy.Attribute).ToArray(); - - IEnumerable targetResources = - LoadDbValues(typeof(TResource), (IEnumerable)result.Node.UniqueResources, ResourceHook.BeforeDelete, relationships) ?? - result.Node.UniqueResources; - - var affected = new ResourceHashSet(targetResources, result.Node.LeftsToNextLayer()); - - IEnumerable updated = result.Container.BeforeDelete(affected, pipeline); - result.Node.UpdateUnique(updated); - result.Node.Reassign(resources); - } - - // If we're deleting an article, we're implicitly affected any owners related to it. - // Here we're loading all relations onto the to-be-deleted article - // if for that relation the BeforeImplicitUpdateHook is implemented, - // and this hook is then executed - foreach (KeyValuePair> entry in result.Node.LeftsToNextLayerByRelationships()) - { - Type rightType = entry.Key; - Dictionary implicitTargets = entry.Value; - FireForAffectedImplicits(rightType, implicitTargets, pipeline); - } - - return resources; - } - - /// - public IEnumerable OnReturn(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - GetHookResult result = GetHook(ResourceHook.OnReturn, resources); - - if (result.Succeeded) - { - IEnumerable updated = result.Container.OnReturn((HashSet)result.Node.UniqueResources, pipeline); - ValidateHookResponse(updated); - result.Node.UpdateUnique(updated); - result.Node.Reassign(resources); - } - - TraverseNodesInLayer(_nodeNavigator.CreateNextLayer(result.Node), ResourceHook.OnReturn, (nextContainer, nextNode) => - { - IEnumerable filteredUniqueSet = CallHook(nextContainer, ResourceHook.OnReturn, ArrayFactory.Create(nextNode.UniqueResources, pipeline)); - nextNode.UpdateUnique(filteredUniqueSet); - nextNode.Reassign(); - }); - - return resources; - } - - /// - public void AfterRead(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - GetHookResult result = GetHook(ResourceHook.AfterRead, resources); - - if (result.Succeeded) - { - result.Container.AfterRead((HashSet)result.Node.UniqueResources, pipeline); - } - - TraverseNodesInLayer(_nodeNavigator.CreateNextLayer(result.Node), ResourceHook.AfterRead, (nextContainer, nextNode) => - { - CallHook(nextContainer, ResourceHook.AfterRead, ArrayFactory.Create(nextNode.UniqueResources, pipeline, true)); - }); - } - - /// - public void AfterCreate(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - GetHookResult result = GetHook(ResourceHook.AfterCreate, resources); - - if (result.Succeeded) - { - result.Container.AfterCreate((HashSet)result.Node.UniqueResources, pipeline); - } - - TraverseNodesInLayer(_nodeNavigator.CreateNextLayer(result.Node), ResourceHook.AfterUpdateRelationship, - (nextContainer, nextNode) => FireAfterUpdateRelationship(nextContainer, nextNode, pipeline)); - } - - /// - public void AfterUpdate(IEnumerable resources, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - GetHookResult result = GetHook(ResourceHook.AfterUpdate, resources); - - if (result.Succeeded) - { - result.Container.AfterUpdate((HashSet)result.Node.UniqueResources, pipeline); - } - - TraverseNodesInLayer(_nodeNavigator.CreateNextLayer(result.Node), ResourceHook.AfterUpdateRelationship, - (nextContainer, nextNode) => FireAfterUpdateRelationship(nextContainer, nextNode, pipeline)); - } - - /// - public void AfterDelete(IEnumerable resources, ResourcePipeline pipeline, bool succeeded) - where TResource : class, IIdentifiable - { - GetHookResult result = GetHook(ResourceHook.AfterDelete, resources); - - if (result.Succeeded) - { - result.Container.AfterDelete((HashSet)result.Node.UniqueResources, pipeline, succeeded); - } - } - - /// - /// For a given target and for a given type , gets the hook container if the target hook was - /// implemented and should be executed. - /// - /// Along the way, creates a traversable node from the root resource set. - /// - /// - /// true, if hook was implemented, false otherwise. - /// - private GetHookResult GetHook(ResourceHook target, IEnumerable resources) - where TResource : class, IIdentifiable - { - RootNode node = _nodeNavigator.CreateRootNode(resources); - IResourceHookContainer container = _containerProvider.GetResourceHookContainer(target); - - return new GetHookResult(container, node); - } - - private void TraverseNodesInLayer(IEnumerable currentLayer, ResourceHook target, Action action) - { - IEnumerable nextLayer = currentLayer; - - while (true) - { - if (!HasResources(nextLayer)) - { - return; - } - - TraverseNextLayer(nextLayer, action, target); - - nextLayer = _nodeNavigator.CreateNextLayer(nextLayer.ToList()); - } - } - - private static bool HasResources(IEnumerable layer) - { - return layer.Any(node => node.UniqueResources.Cast().Any()); - } - - private void TraverseNextLayer(IEnumerable nextLayer, Action action, ResourceHook target) - { - foreach (IResourceNode node in nextLayer) - { - IResourceHookContainer hookContainer = _containerProvider.GetResourceHookContainer(node.ResourceType, target); - - if (hookContainer != null) - { - action(hookContainer, node); - } - } - } - - /// - /// Recursively goes through the included relationships from JsonApiContext, translates them to the corresponding hook containers and fires the - /// BeforeRead hook (if implemented) - /// - private void RecursiveBeforeRead(List relationshipChain, ResourcePipeline pipeline, List calledContainers) - { - while (true) - { - RelationshipAttribute relationship = relationshipChain.First(); - - if (!calledContainers.Contains(relationship.RightType)) - { - calledContainers.Add(relationship.RightType); - IResourceHookContainer container = _containerProvider.GetResourceHookContainer(relationship.RightType, ResourceHook.BeforeRead); - - if (container != null) - { - CallHook(container, ResourceHook.BeforeRead, new object[] - { - pipeline, - true, - null - }); - } - } - - relationshipChain.RemoveAt(0); - - if (!relationshipChain.Any()) - { - break; - } - } - } - - /// - /// Fires the nested before hooks for resources in the current - /// - /// - /// For example: consider the case when the owner of article1 (one-to-one) is being updated from owner_old to owner_new, where owner_new is currently - /// already related to article2. Then, the following nested hooks need to be fired in the following order. First the BeforeUpdateRelationship should be - /// for owner1, then the BeforeImplicitUpdateRelationship hook should be fired for owner2, and lastly the BeforeImplicitUpdateRelationship for article2. - /// - private void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, IEnumerable layer) - { - foreach (IResourceNode node in layer) - { - IResourceHookContainer nestedHookContainer = - _containerProvider.GetResourceHookContainer(node.ResourceType, ResourceHook.BeforeUpdateRelationship); - - IEnumerable uniqueResources = node.UniqueResources; - RightType resourceType = node.ResourceType; - IDictionary currentResourcesGrouped; - IDictionary currentResourcesGroupedInverse; - - // fire the BeforeUpdateRelationship hook for owner_new - if (nestedHookContainer != null) - { - if (uniqueResources.Cast().Any()) - { - RelationshipAttribute[] relationships = node.RelationshipsToNextLayer.Select(proxy => proxy.Attribute).ToArray(); - IEnumerable dbValues = LoadDbValues(resourceType, uniqueResources, ResourceHook.BeforeUpdateRelationship, relationships); - - // these are the resources of the current node grouped by - // RelationshipAttributes that occurred in the previous layer - // so it looks like { HasOneAttribute:owner => owner_new }. - // Note that in the BeforeUpdateRelationship hook of Person, - // we want want inverse relationship attribute: - // we now have the one pointing from article -> person, ] - // but we require the the one that points from person -> article - currentResourcesGrouped = node.RelationshipsFromPreviousLayer.GetRightResources(); - currentResourcesGroupedInverse = ReplaceKeysWithInverseRelationships(currentResourcesGrouped); - - IRelationshipsDictionary resourcesByRelationship = CreateRelationshipHelper(resourceType, currentResourcesGroupedInverse, dbValues); - - IEnumerable allowedIds = CallHook(nestedHookContainer, ResourceHook.BeforeUpdateRelationship, - ArrayFactory.Create(GetIds(uniqueResources), resourcesByRelationship, pipeline)).Cast(); - - ISet updated = GetAllowedResources(uniqueResources, allowedIds); - node.UpdateUnique(updated); - node.Reassign(); - } - } - - // Fire the BeforeImplicitUpdateRelationship hook for owner_old. - // Note: if the pipeline is Post it means we just created article1, - // which means we are sure that it isn't related to any other resources yet. - if (pipeline != ResourcePipeline.Post) - { - // To fire a hook for owner_old, we need to first get a reference to it. - // For this, we need to query the database for the HasOneAttribute:owner - // relationship of article1, which is referred to as the - // left side of the HasOneAttribute:owner relationship. - IDictionary leftResources = node.RelationshipsFromPreviousLayer.GetLeftResources(); - - if (leftResources.Any()) - { - // owner_old is loaded, which is an "implicitly affected resource" - FireForAffectedImplicits(resourceType, leftResources, pipeline, uniqueResources); - } - } - - // Fire the BeforeImplicitUpdateRelationship hook for article2 - // For this, we need to query the database for the current owner - // relationship value of owner_new. - currentResourcesGrouped = node.RelationshipsFromPreviousLayer.GetRightResources(); - - if (currentResourcesGrouped.Any()) - { - // rightResources is grouped by relationships from previous - // layer, ie { HasOneAttribute:owner => owner_new }. But - // to load article2 onto owner_new, we need to have the - // RelationshipAttribute from owner to article, which is the - // inverse of HasOneAttribute:owner - currentResourcesGroupedInverse = ReplaceKeysWithInverseRelationships(currentResourcesGrouped); - // Note that currently in the JsonApiDotNetCore implementation of hooks, - // the root layer is ALWAYS homogenous, so we safely assume - // that for every relationship to the previous layer, the - // left type is the same. - LeftType leftType = currentResourcesGrouped.First().Key.LeftType; - FireForAffectedImplicits(leftType, currentResourcesGroupedInverse, pipeline); - } - } - } - - /// - /// replaces the keys of the dictionary with its inverse relationship attribute. - /// - /// - /// Resources grouped by relationship attribute - /// - private IDictionary ReplaceKeysWithInverseRelationships( - IDictionary resourcesByRelationship) - { - // when Article has one Owner (HasOneAttribute:owner) is set, there is no guarantee - // that the inverse attribute was also set (Owner has one Article: HasOneAttr:article). - // If it isn't, JsonApiDotNetCore currently knows nothing about this relationship pointing back, and it - // currently cannot fire hooks for resources resolved through inverse relationships. - IEnumerable> inversableRelationshipAttributes = - resourcesByRelationship.Where(pair => pair.Key.InverseNavigationProperty != null); - - return inversableRelationshipAttributes.ToDictionary(pair => _resourceGraph.GetInverseRelationship(pair.Key), pair => pair.Value); - } - - /// - /// Given a source of resources, gets the implicitly affected resources from the database and calls the BeforeImplicitUpdateRelationship hook. - /// - private void FireForAffectedImplicits(Type resourceTypeToInclude, IDictionary implicitsTarget, - ResourcePipeline pipeline, IEnumerable existingImplicitResources = null) - { - IResourceHookContainer container = - _containerProvider.GetResourceHookContainer(resourceTypeToInclude, ResourceHook.BeforeImplicitUpdateRelationship); - - if (container == null) - { - return; - } - - IDictionary implicitAffected = - _containerProvider.LoadImplicitlyAffected(implicitsTarget, existingImplicitResources); - - if (!implicitAffected.Any()) - { - return; - } - - Dictionary inverse = - implicitAffected.ToDictionary(pair => _resourceGraph.GetInverseRelationship(pair.Key), pair => pair.Value); - - IRelationshipsDictionary resourcesByRelationship = CreateRelationshipHelper(resourceTypeToInclude, inverse); - CallHook(container, ResourceHook.BeforeImplicitUpdateRelationship, ArrayFactory.Create(resourcesByRelationship, pipeline)); - } - - /// - /// checks that the collection does not contain more than one item when relevant (eg AfterRead from GetSingle pipeline). - /// - /// - /// The collection returned from the hook - /// - /// - /// The pipeline from which the hook was fired - /// - [AssertionMethod] - private void ValidateHookResponse(IEnumerable returnedList, ResourcePipeline pipeline = 0) - { - if (pipeline == ResourcePipeline.GetSingle && returnedList.Count() > 1) - { - throw new InvalidOperationException("The returned collection from this hook may contain at most one item in the case of the " + - pipeline.ToString("G") + " pipeline"); - } - } - - /// - /// A helper method to call a hook on reflectively. - /// - private IEnumerable CallHook(IResourceHookContainer container, ResourceHook hook, object[] arguments) - { - MethodInfo method = container.GetType().GetMethod(hook.ToString("G")); - // note that some of the hooks return "void". When these hooks, the - // are called reflectively with Invoke like here, the return value - // is just null, so we don't have to worry about casting issues here. - return (IEnumerable)ThrowJsonApiExceptionOnError(() => method?.Invoke(container, arguments)); - } - - /// - /// If the method, unwrap and throw the actual exception. - /// - private object ThrowJsonApiExceptionOnError(Func action) - { - try - { - return action(); - } - catch (TargetInvocationException tie) when (tie.InnerException != null) - { - throw tie.InnerException; - } - } - - /// - /// Helper method to instantiate AffectedRelationships for a given If are included, the - /// values of the entries in need to be replaced with these values. - /// - /// - /// The relationship helper. - /// - private IRelationshipsDictionary CreateRelationshipHelper(RightType resourceType, - IDictionary prevLayerRelationships, IEnumerable dbValues = null) - { - IDictionary prevLayerRelationshipsWithDbValues = prevLayerRelationships; - - if (dbValues != null) - { - prevLayerRelationshipsWithDbValues = ReplaceWithDbValues(prevLayerRelationshipsWithDbValues, dbValues.Cast()); - } - - return (IRelationshipsDictionary)ObjectFactory.CreateInstanceOfInternalOpenType(typeof(RelationshipsDictionary<>), resourceType, - prevLayerRelationshipsWithDbValues); - } - - /// - /// Replaces the resources in the values of the prevLayerRelationships dictionary with the corresponding resources loaded from the db. - /// - private IDictionary ReplaceWithDbValues(IDictionary prevLayerRelationships, - IEnumerable dbValues) - { - foreach (RelationshipAttribute key in prevLayerRelationships.Keys.ToList()) - { - IEnumerable source = prevLayerRelationships[key].Cast().Select(resource => - dbValues.Single(dbResource => dbResource.StringId == resource.StringId)); - - prevLayerRelationships[key] = CollectionConverter.CopyToHashSet(source, key.LeftType); - } - - return prevLayerRelationships; - } - - /// - /// Filter the source set by removing the resources with ID that are not in . - /// - private ISet GetAllowedResources(IEnumerable source, IEnumerable allowedIds) - { - return new HashSet(source.Cast().Where(ue => allowedIds.Contains(ue.StringId))); - } - - /// - /// given the set of , it will load all the values from the database of these resources. - /// - /// - /// The db values. - /// - /// - /// type of the resources to be loaded - /// - /// - /// The set of resources to load the db values for - /// - /// - /// The hook in which the db values will be displayed. - /// - /// - /// Relationships from to the next layer: this indicates which relationships will be included on - /// . - /// - private IEnumerable LoadDbValues(Type resourceType, IEnumerable uniqueResources, ResourceHook targetHook, - RelationshipAttribute[] relationshipsToNextLayer) - { - // We only need to load database values if the target hook of this hook execution - // cycle is compatible with displaying database values and has this option enabled. - if (!_containerProvider.ShouldLoadDbValues(resourceType, targetHook)) - { - return null; - } - - return _containerProvider.LoadDbValues(resourceType, uniqueResources, relationshipsToNextLayer); - } - - /// - /// Fires the AfterUpdateRelationship hook - /// - private void FireAfterUpdateRelationship(IResourceHookContainer container, IResourceNode node, ResourcePipeline pipeline) - { - IDictionary currentResourcesGrouped = node.RelationshipsFromPreviousLayer.GetRightResources(); - - // the relationships attributes in currentResourcesGrouped will be pointing from a - // resource in the previous layer to a resource in the current (nested) layer. - // For the nested hook we need to replace these attributes with their inverse. - // See the FireNestedBeforeUpdateHooks method for a more detailed example. - IRelationshipsDictionary resourcesByRelationship = - CreateRelationshipHelper(node.ResourceType, ReplaceKeysWithInverseRelationships(currentResourcesGrouped)); - - CallHook(container, ResourceHook.AfterUpdateRelationship, ArrayFactory.Create(resourcesByRelationship, pipeline)); - } - - /// - /// Returns a list of StringIds from a list of IIdentifiable resources (). - /// - /// The ids. - /// - /// IIdentifiable resources. - /// - private ISet GetIds(IEnumerable resources) - { - return new HashSet(resources.Cast().Select(resource => resource.StringId)); - } - - private sealed class GetHookResult - where TResource : class, IIdentifiable - { - public IResourceHookContainer Container { get; } - public RootNode Node { get; } - - public bool Succeeded => Container != null; - - public GetHookResult(IResourceHookContainer container, RootNode node) - { - Container = container; - Node = node; - } - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutorFacade.cs b/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutorFacade.cs deleted file mode 100644 index 21c03ab33f..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutorFacade.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Hooks.Internal -{ - /// - /// Facade for hooks that invokes callbacks on , which is used when - /// is true. - /// - internal sealed class ResourceHookExecutorFacade : IResourceHookExecutorFacade - { - private readonly IResourceHookExecutor _resourceHookExecutor; - private readonly IResourceFactory _resourceFactory; - - public ResourceHookExecutorFacade(IResourceHookExecutor resourceHookExecutor, IResourceFactory resourceFactory) - { - ArgumentGuard.NotNull(resourceHookExecutor, nameof(resourceHookExecutor)); - ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); - - _resourceHookExecutor = resourceHookExecutor; - _resourceFactory = resourceFactory; - } - - public void BeforeReadSingle(TId id, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - var temporaryResource = _resourceFactory.CreateInstance(); - temporaryResource.Id = id; - - _resourceHookExecutor.BeforeRead(pipeline, temporaryResource.StringId); - } - - public void AfterReadSingle(TResource resource, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - _resourceHookExecutor.AfterRead(resource.AsList(), pipeline); - } - - public void BeforeReadMany() - where TResource : class, IIdentifiable - { - _resourceHookExecutor.BeforeRead(ResourcePipeline.Get); - } - - public void AfterReadMany(IReadOnlyCollection resources) - where TResource : class, IIdentifiable - { - _resourceHookExecutor.AfterRead(resources, ResourcePipeline.Get); - } - - public void BeforeCreate(TResource resource) - where TResource : class, IIdentifiable - { - _resourceHookExecutor.BeforeCreate(resource.AsList(), ResourcePipeline.Post); - } - - public void AfterCreate(TResource resource) - where TResource : class, IIdentifiable - { - _resourceHookExecutor.AfterCreate(resource.AsList(), ResourcePipeline.Post); - } - - public void BeforeUpdateResource(TResource resource) - where TResource : class, IIdentifiable - { - _resourceHookExecutor.BeforeUpdate(resource.AsList(), ResourcePipeline.Patch); - } - - public void AfterUpdateResource(TResource resource) - where TResource : class, IIdentifiable - { - _resourceHookExecutor.AfterUpdate(resource.AsList(), ResourcePipeline.Patch); - } - - public void BeforeUpdateRelationship(TResource resource) - where TResource : class, IIdentifiable - { - _resourceHookExecutor.BeforeUpdate(resource.AsList(), ResourcePipeline.PatchRelationship); - } - - public void AfterUpdateRelationship(TResource resource) - where TResource : class, IIdentifiable - { - _resourceHookExecutor.AfterUpdate(resource.AsList(), ResourcePipeline.PatchRelationship); - } - - public void BeforeDelete(TId id) - where TResource : class, IIdentifiable - { - var temporaryResource = _resourceFactory.CreateInstance(); - temporaryResource.Id = id; - - _resourceHookExecutor.BeforeDelete(temporaryResource.AsList(), ResourcePipeline.Delete); - } - - public void AfterDelete(TId id) - where TResource : class, IIdentifiable - { - var temporaryResource = _resourceFactory.CreateInstance(); - temporaryResource.Id = id; - - _resourceHookExecutor.AfterDelete(temporaryResource.AsList(), ResourcePipeline.Delete, true); - } - - public void OnReturnSingle(TResource resource, ResourcePipeline pipeline) - where TResource : class, IIdentifiable - { - _resourceHookExecutor.OnReturn(resource.AsList(), pipeline); - } - - public IReadOnlyCollection OnReturnMany(IReadOnlyCollection resources) - where TResource : class, IIdentifiable - { - return _resourceHookExecutor.OnReturn(resources, ResourcePipeline.Get).ToArray(); - } - - public object OnReturnRelationship(object resourceOrResources) - { - if (resourceOrResources is IEnumerable) - { - dynamic resources = resourceOrResources; - return Enumerable.ToArray(_resourceHookExecutor.OnReturn(resources, ResourcePipeline.GetRelationship)); - } - - if (resourceOrResources is IIdentifiable) - { - dynamic resources = ObjectExtensions.AsList((dynamic)resourceOrResources); - return Enumerable.SingleOrDefault(_resourceHookExecutor.OnReturn(resources, ResourcePipeline.GetRelationship)); - } - - return resourceOrResources; - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs deleted file mode 100644 index 7bf2209118..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Resources; -using RightType = System.Type; - -namespace JsonApiDotNetCore.Hooks.Internal.Traversal -{ - internal abstract class ChildNode - { - protected static readonly CollectionConverter CollectionConverter = new CollectionConverter(); - } - - /// - /// Child node in the tree - /// - /// - internal sealed class ChildNode : ChildNode, IResourceNode - where TResource : class, IIdentifiable - { - private readonly IdentifiableComparer _comparer = IdentifiableComparer.Instance; - private readonly RelationshipsFromPreviousLayer _relationshipsFromPreviousLayer; - - /// - public RightType ResourceType { get; } - - /// - public IReadOnlyCollection RelationshipsToNextLayer { get; } - - /// - public IEnumerable UniqueResources - { - get - { - return new HashSet(_relationshipsFromPreviousLayer.SelectMany(relationshipGroup => relationshipGroup.RightResources)); - } - } - - /// - public IRelationshipsFromPreviousLayer RelationshipsFromPreviousLayer => _relationshipsFromPreviousLayer; - - public ChildNode(IReadOnlyCollection nextLayerRelationships, RelationshipsFromPreviousLayer prevLayerRelationships) - { - ResourceType = typeof(TResource); - RelationshipsToNextLayer = nextLayerRelationships; - _relationshipsFromPreviousLayer = prevLayerRelationships; - } - - /// - public void UpdateUnique(IEnumerable updated) - { - List list = updated.Cast().ToList(); - - foreach (RelationshipGroup group in _relationshipsFromPreviousLayer) - { - group.RightResources = new HashSet(group.RightResources.Intersect(list, _comparer).Cast()); - } - } - - /// - /// Reassignment is done according to provided relationships - /// - public void Reassign(IEnumerable updated = null) - { - var unique = (HashSet)UniqueResources; - - foreach (RelationshipGroup group in _relationshipsFromPreviousLayer) - { - RelationshipProxy proxy = group.Proxy; - HashSet leftResources = group.LeftResources; - - Reassign(leftResources, proxy, unique); - } - } - - private void Reassign(IEnumerable leftResources, RelationshipProxy proxy, HashSet unique) - { - foreach (IIdentifiable left in leftResources) - { - object currentValue = proxy.GetValue(left); - - if (currentValue is IEnumerable relationshipCollection) - { - IEnumerable intersection = relationshipCollection.Intersect(unique, _comparer); - IEnumerable typedCollection = CollectionConverter.CopyToTypedCollection(intersection, relationshipCollection.GetType()); - proxy.SetValue(left, typedCollection); - } - else if (currentValue is IIdentifiable relationshipSingle) - { - if (!unique.Intersect(new HashSet - { - relationshipSingle - }, _comparer).Any()) - { - proxy.SetValue(left, null); - } - } - } - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/INodeNavigator.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/INodeNavigator.cs deleted file mode 100644 index 1b01751736..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/INodeNavigator.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Hooks.Internal.Traversal -{ - internal interface INodeNavigator - { - /// - /// Creates the next layer - /// - IEnumerable CreateNextLayer(IResourceNode node); - - /// - /// Creates the next layer based on the nodes provided - /// - IEnumerable CreateNextLayer(IEnumerable nodes); - - /// - /// Creates a root node for breadth-first-traversal (BFS). Note that typically, in JsonApiDotNetCore, the root layer will be homogeneous. Also, because - /// it is the first layer, there can be no relationships to previous layers, only to next layers. - /// - /// - /// The root node. - /// - /// - /// Root resources. - /// - /// - /// The 1st type parameter. - /// - RootNode CreateRootNode(IEnumerable rootResources) - where TResource : class, IIdentifiable; - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipGroup.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipGroup.cs deleted file mode 100644 index 3dc9c9a8fa..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipGroup.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace JsonApiDotNetCore.Hooks.Internal.Traversal -{ - internal interface IRelationshipGroup - { - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipsFromPreviousLayer.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipsFromPreviousLayer.cs deleted file mode 100644 index ff8795aaa6..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipsFromPreviousLayer.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Hooks.Internal.Traversal -{ - /// - /// A helper class for mapping relationships between a current and previous layer - /// - internal interface IRelationshipsFromPreviousLayer - { - /// - /// Grouped by relationship to the previous layer, gets all the resources of the current layer - /// - /// - /// The right side resources. - /// - IDictionary GetRightResources(); - - /// - /// Grouped by relationship to the previous layer, gets all the resources of the previous layer - /// - /// - /// The right side resources. - /// - IDictionary GetLeftResources(); - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IResourceNode.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IResourceNode.cs deleted file mode 100644 index d86fb05431..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IResourceNode.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using RightType = System.Type; - -namespace JsonApiDotNetCore.Hooks.Internal.Traversal -{ - /// - /// This is the interface that nodes need to inherit from - /// - internal interface IResourceNode - { - /// - /// Each node represents the resources of a given type throughout a particular layer. - /// - RightType ResourceType { get; } - - /// - /// The unique set of resources in this node. Note that these are all of the same type. - /// - IEnumerable UniqueResources { get; } - - /// - /// Relationships to the next layer - /// - /// - /// The relationships to next layer. - /// - IReadOnlyCollection RelationshipsToNextLayer { get; } - - /// - /// Relationships to the previous layer - /// - IRelationshipsFromPreviousLayer RelationshipsFromPreviousLayer { get; } - - /// - /// A helper method to assign relationships to the previous layer after firing hooks. Or, in case of the root node, to update the original source - /// enumerable. - /// - void Reassign(IEnumerable source = null); - - /// - /// A helper method to internally update the unique set of resources as a result of a filter action in a hook. - /// - /// Updated. - void UpdateUnique(IEnumerable updated); - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/NodeNavigator.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/NodeNavigator.cs deleted file mode 100644 index ac4435ac7e..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/NodeNavigator.cs +++ /dev/null @@ -1,377 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using RightType = System.Type; -using LeftType = System.Type; - -namespace JsonApiDotNetCore.Hooks.Internal.Traversal -{ - /// - /// A helper class used by the to traverse through resource data structures (trees), allowing for a - /// breadth-first-traversal It creates nodes for each layer. Typically, the first layer is homogeneous (all resources have the same type), and further - /// nodes can be mixed. - /// - internal sealed class NodeNavigator : INodeNavigator - { - private static readonly HooksObjectFactory ObjectFactory = new HooksObjectFactory(); - private static readonly HooksCollectionConverter CollectionConverter = new HooksCollectionConverter(); - - private readonly IdentifiableComparer _comparer = IdentifiableComparer.Instance; - private readonly IResourceGraph _resourceGraph; - private readonly ITargetedFields _targetedFields; - - /// - /// A mapper from to . See the latter for more details. - /// - private readonly Dictionary _relationshipProxies = new Dictionary(); - - /// - /// Keeps track of which resources has already been traversed through, to prevent infinite loops in eg cyclic data structures. - /// - private Dictionary> _processedResources; - - public NodeNavigator(IResourceGraph resourceGraph, ITargetedFields targetedFields) - { - _targetedFields = targetedFields; - _resourceGraph = resourceGraph; - } - - /// - /// Creates a root node for breadth-first-traversal. Note that typically, in JsonApiDotNetCore, the root layer will be homogeneous. Also, because it is - /// the first layer, there can be no relationships to previous layers, only to next layers. - /// - /// - /// The root node. - /// - /// - /// Root resources. - /// - /// - /// The 1st type parameter. - /// - public RootNode CreateRootNode(IEnumerable rootResources) - where TResource : class, IIdentifiable - { - _processedResources = new Dictionary>(); - RegisterRelationshipProxies(typeof(TResource)); - ISet uniqueResources = ProcessResources(rootResources); - IReadOnlyCollection populatedRelationshipsToNextLayer = GetPopulatedRelationships(typeof(TResource), uniqueResources); - - IReadOnlyCollection allRelationshipsFromType = - _relationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.LeftType == typeof(TResource)).ToArray(); - - return new RootNode(uniqueResources, populatedRelationshipsToNextLayer, allRelationshipsFromType); - } - - /// - /// Create the first layer after the root layer (based on the root node) - /// - /// - /// The next layer. - /// - /// - /// Root node. - /// - public IEnumerable CreateNextLayer(IResourceNode rootNode) - { - return CreateNextLayer(rootNode.AsEnumerable()); - } - - /// - /// Create a next layer from any previous layer - /// - /// - /// The next layer. - /// - /// Nodes. - public IEnumerable CreateNextLayer(IEnumerable nodes) - { - // first extract resources by parsing populated relationships in the resources - // of previous layer - (Dictionary> lefts, Dictionary> rights) = ExtractResources(nodes); - - // group them conveniently so we can make ChildNodes of them: - // there might be several relationship attributes in rights dictionary - // that point to the same right type. - IDictionary>>> leftsGrouped = GroupByRightTypeOfRelationship(lefts); - - // convert the groups into child nodes - List nextNodes = leftsGrouped.Select(entry => - { - RightType nextNodeType = entry.Key; - RegisterRelationshipProxies(nextNodeType); - - var populatedRelationships = new List(); - - List relationshipsToPreviousLayer = entry.Value.Select(grouped => - { - RelationshipProxy proxy = grouped.Key; - populatedRelationships.AddRange(GetPopulatedRelationships(nextNodeType, rights[proxy])); - return CreateRelationshipGroupInstance(nextNodeType, proxy, grouped.Value, rights[proxy]); - }).ToList(); - - return CreateNodeInstance(nextNodeType, populatedRelationships.ToArray(), relationshipsToPreviousLayer); - }).ToList(); - - return nextNodes; - } - - /// - /// iterates through the dictionary and groups the values by matching right type of the keys (which are relationship - /// attributes) - /// - private IDictionary>>> GroupByRightTypeOfRelationship( - Dictionary> relationships) - { - return relationships.GroupBy(pair => pair.Key.RightType).ToDictionary(grouping => grouping.Key, grouping => grouping.ToList()); - } - - /// - /// Extracts the resources for the current layer by going through all populated relationships of the (left resources of the previous layer. - /// - private (Dictionary>, Dictionary>) ExtractResources( - IEnumerable leftNodes) - { - // RelationshipAttr_prevLayer->currentLayer => prevLayerResources - var leftResourcesGrouped = new Dictionary>(); - - // RelationshipAttr_prevLayer->currentLayer => currentLayerResources - var rightResourcesGrouped = new Dictionary>(); - - foreach (IResourceNode node in leftNodes) - { - IEnumerable leftResources = node.UniqueResources; - IReadOnlyCollection relationships = node.RelationshipsToNextLayer; - - ExtractLeftResources(leftResources, relationships, rightResourcesGrouped, leftResourcesGrouped); - } - - MethodInfo processResourcesMethod = GetType().GetMethod(nameof(ProcessResources), BindingFlags.NonPublic | BindingFlags.Instance); - - foreach (KeyValuePair> pair in rightResourcesGrouped) - { - RightType type = pair.Key.RightType; - IList list = CollectionConverter.CopyToList(pair.Value, type); - processResourcesMethod!.MakeGenericMethod(type).Invoke(this, ArrayFactory.Create(list)); - } - - return (leftResourcesGrouped, rightResourcesGrouped); - } - - private void ExtractLeftResources(IEnumerable leftResources, IReadOnlyCollection relationships, - Dictionary> rightResourcesGrouped, Dictionary> leftResourcesGrouped) - { - foreach (IIdentifiable leftResource in leftResources) - { - ExtractLeftResource(leftResource, relationships, rightResourcesGrouped, leftResourcesGrouped); - } - } - - private void ExtractLeftResource(IIdentifiable leftResource, IReadOnlyCollection relationships, - Dictionary> rightResourcesGrouped, Dictionary> leftResourcesGrouped) - { - foreach (RelationshipProxy proxy in relationships) - { - object relationshipValue = proxy.GetValue(leftResource); - - // skip this relationship if it's not populated - if (!proxy.IsContextRelation && relationshipValue == null) - { - continue; - } - - ICollection rightResources = CollectionConverter.ExtractResources(relationshipValue); - ISet uniqueRightResources = UniqueInTree(rightResources, proxy.RightType); - - if (proxy.IsContextRelation || uniqueRightResources.Any()) - { - AddToRelationshipGroup(rightResourcesGrouped, proxy, uniqueRightResources); - AddToRelationshipGroup(leftResourcesGrouped, proxy, leftResource.AsEnumerable()); - } - } - } - - /// - /// Get all populated relationships known in the current tree traversal from a left type to any right type - /// - /// - /// The relationships. - /// - private IReadOnlyCollection GetPopulatedRelationships(LeftType leftType, IEnumerable lefts) - { - IEnumerable relationshipsFromLeftToRight = - _relationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.LeftType == leftType); - - return relationshipsFromLeftToRight.Where(proxy => proxy.IsContextRelation || lefts.Any(resource => proxy.GetValue(resource) != null)).ToArray(); - } - - /// - /// Registers the resources as "seen" in the tree traversal, extracts any new s from it. - /// - /// - /// The resources. - /// - /// - /// Incoming resources. - /// - /// - /// The 1st type parameter. - /// - private ISet ProcessResources(IEnumerable incomingResources) - where TResource : class, IIdentifiable - { - RightType type = typeof(TResource); - ISet newResources = UniqueInTree(incomingResources, type); - RegisterProcessedResources(newResources, type); - return newResources; - } - - /// - /// Parses all relationships from to other models in the resource resourceGraphs by constructing RelationshipProxies . - /// - /// - /// The type to parse - /// - private void RegisterRelationshipProxies(RightType type) - { - foreach (RelationshipAttribute attr in _resourceGraph.GetRelationships(type)) - { - if (!attr.CanInclude) - { - continue; - } - - if (!_relationshipProxies.TryGetValue(attr, out _)) - { - RightType rightType = GetRightTypeFromRelationship(attr); - bool isContextRelation = false; - ISet relationshipsToUpdate = _targetedFields.Relationships; - - if (relationshipsToUpdate != null) - { - isContextRelation = relationshipsToUpdate.Contains(attr); - } - - var proxy = new RelationshipProxy(attr, rightType, isContextRelation); - _relationshipProxies[attr] = proxy; - } - } - } - - /// - /// Registers the processed resources in the dictionary grouped by type - /// - /// - /// Resources to register - /// - /// - /// Resource type. - /// - private void RegisterProcessedResources(IEnumerable resources, RightType resourceType) - { - ISet processedResources = GetProcessedResources(resourceType); - processedResources.UnionWith(new HashSet(resources)); - } - - /// - /// Gets the processed resources for a given type, instantiates the collection if new. - /// - /// - /// The processed resources. - /// - /// - /// Resource type. - /// - private ISet GetProcessedResources(RightType resourceType) - { - if (!_processedResources.TryGetValue(resourceType, out HashSet processedResources)) - { - processedResources = new HashSet(); - _processedResources[resourceType] = processedResources; - } - - return processedResources; - } - - /// - /// Using the register of processed resources, determines the unique and new resources with respect to previous iterations. - /// - /// - /// The in tree. - /// - private ISet UniqueInTree(IEnumerable resources, RightType resourceType) - where TResource : class, IIdentifiable - { - IEnumerable newResources = resources.Except(GetProcessedResources(resourceType), _comparer).Cast(); - return new HashSet(newResources); - } - - /// - /// Gets the type from relationship attribute. If the attribute is HasManyThrough, and the through type is identifiable, then the target type is the - /// through type instead of the right type, because hooks might be implemented for the through resource. - /// - /// - /// The target type for traversal - /// - /// - /// Relationship attribute - /// - private RightType GetRightTypeFromRelationship(RelationshipAttribute attr) - { - if (attr is HasManyThroughAttribute throughAttr && throughAttr.ThroughType.IsOrImplementsInterface(typeof(IIdentifiable))) - { - return throughAttr.ThroughType; - } - - return attr.RightType; - } - - private void AddToRelationshipGroup(Dictionary> target, RelationshipProxy proxy, - IEnumerable newResources) - { - if (!target.TryGetValue(proxy, out List resources)) - { - resources = new List(); - target[proxy] = resources; - } - - resources.AddRange(newResources); - } - - /// - /// Reflective helper method to create an instance of ; - /// - private IResourceNode CreateNodeInstance(RightType nodeType, IReadOnlyCollection relationshipsToNext, - IEnumerable relationshipsFromPrev) - { - IRelationshipsFromPreviousLayer prev = CreateRelationshipsFromInstance(nodeType, relationshipsFromPrev); - return (IResourceNode)ObjectFactory.CreateInstanceOfOpenType(typeof(ChildNode<>), nodeType, relationshipsToNext, prev); - } - - /// - /// Reflective helper method to create an instance of ; - /// - private IRelationshipsFromPreviousLayer CreateRelationshipsFromInstance(RightType nodeType, IEnumerable relationshipsFromPrev) - { - List relationshipsFromPrevList = relationshipsFromPrev.ToList(); - IList list = CollectionConverter.CopyToList(relationshipsFromPrevList, relationshipsFromPrevList.First().GetType()); - return (IRelationshipsFromPreviousLayer)ObjectFactory.CreateInstanceOfOpenType(typeof(RelationshipsFromPreviousLayer<>), nodeType, list); - } - - /// - /// Reflective helper method to create an instance of ; - /// - private IRelationshipGroup CreateRelationshipGroupInstance(RightType thisLayerType, RelationshipProxy proxy, List leftResources, - List rightResources) - { - IEnumerable rightResourceSet = CollectionConverter.CopyToHashSet(rightResources, thisLayerType); - - return (IRelationshipGroup)ObjectFactory.CreateInstanceOfOpenType(typeof(RelationshipGroup<>), thisLayerType, proxy, - new HashSet(leftResources), rightResourceSet); - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipGroup.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipGroup.cs deleted file mode 100644 index 69a947f9f9..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipGroup.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Hooks.Internal.Traversal -{ - internal sealed class RelationshipGroup : IRelationshipGroup - where TRight : class, IIdentifiable - { - public RelationshipProxy Proxy { get; } - public HashSet LeftResources { get; } - public HashSet RightResources { get; internal set; } - - public RelationshipGroup(RelationshipProxy proxy, HashSet leftResources, HashSet rightResources) - { - Proxy = proxy; - LeftResources = leftResources; - RightResources = rightResources; - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs deleted file mode 100644 index b9cd252b4e..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Hooks.Internal.Traversal -{ - /// - /// A class used internally for resource hook execution. Not intended for developer use. A wrapper for RelationshipAttribute with an abstraction layer - /// that works on the getters and setters of relationships. These are different in the case of HasMany vs HasManyThrough, and HasManyThrough. It also - /// depends on if the through type (eg ArticleTags) is identifiable (in which case we will traverse through it and fire hooks for it, if defined) or not - /// (in which case we skip ArticleTags and go directly to Tags. - /// - internal sealed class RelationshipProxy - { - private static readonly HooksCollectionConverter CollectionConverter = new HooksCollectionConverter(); - - private readonly bool _skipThroughType; - - public Type LeftType => Attribute.LeftType; - - /// - /// The target type for this relationship attribute. For HasOne has HasMany this is trivial: just the right-hand side. For HasManyThrough it is either - /// the ThroughProperty (when the through resource is Identifiable) or it is the right-hand side (when the through resource is not identifiable) - /// - public Type RightType { get; } - - public bool IsContextRelation { get; } - - public RelationshipAttribute Attribute { get; } - - public RelationshipProxy(RelationshipAttribute attr, Type relatedType, bool isContextRelation) - { - RightType = relatedType; - Attribute = attr; - IsContextRelation = isContextRelation; - - if (attr is HasManyThroughAttribute throughAttr) - { - _skipThroughType |= RightType != throughAttr.ThroughType; - } - } - - /// - /// Gets the relationship value for a given parent resource. Internally knows how to do this depending on the type of RelationshipAttribute that this - /// RelationshipProxy encapsulates. - /// - /// - /// The relationship value. - /// - /// - /// Parent resource. - /// - public object GetValue(IIdentifiable resource) - { - if (Attribute is HasManyThroughAttribute hasManyThrough) - { - if (!_skipThroughType) - { - return hasManyThrough.ThroughProperty.GetValue(resource); - } - - var collection = new List(); - var throughResources = (IEnumerable)hasManyThrough.ThroughProperty.GetValue(resource); - - if (throughResources == null) - { - return null; - } - - foreach (object throughResource in throughResources) - { - var rightResource = (IIdentifiable)hasManyThrough.RightProperty.GetValue(throughResource); - - if (rightResource == null) - { - continue; - } - - collection.Add(rightResource); - } - - return collection; - } - - return Attribute.GetValue(resource); - } - - /// - /// Set the relationship value for a given parent resource. Internally knows how to do this depending on the type of RelationshipAttribute that this - /// RelationshipProxy encapsulates. - /// - /// - /// Parent resource. - /// - /// - /// The relationship value. - /// - public void SetValue(IIdentifiable resource, object value) - { - if (Attribute is HasManyThroughAttribute hasManyThrough) - { - if (!_skipThroughType) - { - hasManyThrough.ThroughProperty.SetValue(resource, value); - return; - } - - var throughResources = (IEnumerable)hasManyThrough.ThroughProperty.GetValue(resource); - - var filteredList = new List(); - - IList rightResources = CollectionConverter.CopyToList((IEnumerable)value, RightType); - - foreach (object throughResource in throughResources ?? Array.Empty()) - { - if (rightResources.Contains(hasManyThrough.RightProperty.GetValue(throughResource))) - { - filteredList.Add(throughResource); - } - } - - IEnumerable collectionValue = CollectionConverter.CopyToTypedCollection(filteredList, hasManyThrough.ThroughProperty.PropertyType); - hasManyThrough.ThroughProperty.SetValue(resource, collectionValue); - return; - } - - Attribute.SetValue(resource, value); - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipsFromPreviousLayer.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipsFromPreviousLayer.cs deleted file mode 100644 index 9a91205628..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipsFromPreviousLayer.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Hooks.Internal.Traversal -{ - internal sealed class RelationshipsFromPreviousLayer : IRelationshipsFromPreviousLayer, IEnumerable> - where TRightResource : class, IIdentifiable - { - private readonly IEnumerable> _collection; - - public RelationshipsFromPreviousLayer(IEnumerable> collection) - { - _collection = collection; - } - - /// - public IDictionary GetRightResources() - { - return _collection.ToDictionary(rg => rg.Proxy.Attribute, rg => (IEnumerable)rg.RightResources); - } - - /// - public IDictionary GetLeftResources() - { - return _collection.ToDictionary(rg => rg.Proxy.Attribute, rg => (IEnumerable)rg.LeftResources); - } - - public IEnumerator> GetEnumerator() - { - return _collection.Cast>().GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs deleted file mode 100644 index a4453dbace..0000000000 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Hooks.Internal.Traversal -{ - /// - /// The root node class of the breadth-first-traversal of resource data structures as performed by the - /// - internal sealed class RootNode : IResourceNode - where TResource : class, IIdentifiable - { - private readonly IdentifiableComparer _comparer = IdentifiableComparer.Instance; - private readonly IReadOnlyCollection _allRelationshipsToNextLayer; - private HashSet _uniqueResources; - public Type ResourceType { get; } - public IEnumerable UniqueResources => _uniqueResources; - public IReadOnlyCollection RelationshipsToNextLayer { get; } - - /// - /// The root node does not have a parent layer and therefore does not have any relationships to any previous layer - /// - public IRelationshipsFromPreviousLayer RelationshipsFromPreviousLayer => null; - - public RootNode(IEnumerable uniqueResources, IReadOnlyCollection populatedRelationships, - IReadOnlyCollection allRelationships) - { - ResourceType = typeof(TResource); - _uniqueResources = new HashSet(uniqueResources); - RelationshipsToNextLayer = populatedRelationships; - _allRelationshipsToNextLayer = allRelationships; - } - - public IDictionary> LeftsToNextLayerByRelationships() - { - return _allRelationshipsToNextLayer.GroupBy(proxy => proxy.RightType).ToDictionary(grouping => grouping.Key, - grouping => grouping.ToDictionary(proxy => proxy.Attribute, _ => UniqueResources)); - } - - /// - /// The current layer resources grouped by affected relationship to the next layer - /// - public IDictionary LeftsToNextLayer() - { - return RelationshipsToNextLayer.ToDictionary(proxy => proxy.Attribute, _ => UniqueResources); - } - - /// - /// Update the internal list of affected resources. - /// - /// Updated. - public void UpdateUnique(IEnumerable updated) - { - List list = updated.Cast().ToList(); - IEnumerable intersected = _uniqueResources.Intersect(list, _comparer).Cast(); - _uniqueResources = new HashSet(intersected); - } - - public void Reassign(IEnumerable source = null) - { - IEnumerable ids = _uniqueResources.Select(ue => ue.StringId); - - if (source is HashSet hashSet) - { - hashSet.RemoveWhere(se => !ids.Contains(se.StringId)); - } - else if (source is List list) - { - list.RemoveAll(se => !ids.Contains(se.StringId)); - } - else if (source != null) - { - throw new NotSupportedException($"Unsupported collection type '{source.GetType()}'."); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs b/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs deleted file mode 100644 index 60b9e67505..0000000000 --- a/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Execution; - -namespace JsonApiDotNetCore.Resources -{ - /// - /// Provides a resource-specific extensibility point for API developers to be notified of various events and influence behavior using custom code. It is - /// intended to improve the developer experience and reduce boilerplate for commonly required features. The goal of this class is to reduce the frequency - /// with which developers have to override the service and repository layers. - /// - /// - /// The resource type. - /// - [PublicAPI] - public class ResourceHooksDefinition : IResourceHookContainer - where TResource : class, IIdentifiable - { - protected IResourceGraph ResourceGraph { get; } - - public ResourceHooksDefinition(IResourceGraph resourceGraph) - { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - - ResourceGraph = resourceGraph; - } - - /// - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - /// - public virtual void AfterCreate(HashSet resources, ResourcePipeline pipeline) - { - } - - /// - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - /// - public virtual void AfterRead(HashSet resources, ResourcePipeline pipeline, bool isIncluded = false) - { - } - - /// - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - /// - public virtual void AfterUpdate(HashSet resources, ResourcePipeline pipeline) - { - } - - /// - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - /// - public virtual void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) - { - } - - /// - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - /// - public virtual void AfterUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) - { - } - - /// - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - /// - public virtual IEnumerable BeforeCreate(IResourceHashSet resources, ResourcePipeline pipeline) - { - return resources; - } - - /// - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - /// - public virtual void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) - { - } - - /// - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - /// - public virtual IEnumerable BeforeUpdate(IDiffableResourceHashSet resources, ResourcePipeline pipeline) - { - return resources; - } - - /// - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - /// - public virtual IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) - { - return resources; - } - - /// - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - /// - public virtual IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, - ResourcePipeline pipeline) - { - return ids; - } - - /// - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - /// - public virtual void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) - { - } - - /// - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - /// - public virtual IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline) - { - return resources; - } - } -} diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index 26d0f39a70..8500557c1b 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -8,8 +8,6 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; @@ -33,12 +31,11 @@ public class JsonApiResourceService : IResourceService> _traceWriter; private readonly IJsonApiRequest _request; private readonly IResourceChangeTracker _resourceChangeTracker; - private readonly IResourceHookExecutorFacade _hookExecutor; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, - IResourceChangeTracker resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) + IResourceChangeTracker resourceChangeTracker) { ArgumentGuard.NotNull(repositoryAccessor, nameof(repositoryAccessor)); ArgumentGuard.NotNull(queryLayerComposer, nameof(queryLayerComposer)); @@ -47,7 +44,6 @@ public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQ ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(resourceChangeTracker, nameof(resourceChangeTracker)); - ArgumentGuard.NotNull(hookExecutor, nameof(hookExecutor)); _repositoryAccessor = repositoryAccessor; _queryLayerComposer = queryLayerComposer; @@ -55,7 +51,6 @@ public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQ _options = options; _request = request; _resourceChangeTracker = resourceChangeTracker; - _hookExecutor = hookExecutor; _traceWriter = new TraceLogWriter>(loggerFactory); #pragma warning disable 612 // Method is obsolete @@ -68,8 +63,6 @@ public virtual async Task> GetAsync(CancellationT { _traceWriter.LogMethodStart(); - _hookExecutor.BeforeReadMany(); - if (_options.IncludeTotalResourceCount) { FilterExpression topFilter = _queryLayerComposer.GetTopFilterFromConstraints(_request.PrimaryResource); @@ -89,8 +82,7 @@ public virtual async Task> GetAsync(CancellationT _paginationContext.IsPageFull = true; } - _hookExecutor.AfterReadMany(resources); - return _hookExecutor.OnReturnMany(resources); + return resources; } /// @@ -101,14 +93,7 @@ public virtual async Task GetAsync(TId id, CancellationToken cancella id }); - _hookExecutor.BeforeReadSingle(id, ResourcePipeline.GetSingle); - - TResource primaryResource = await GetPrimaryResourceByIdAsync(id, TopFieldSelection.PreserveExisting, cancellationToken); - - _hookExecutor.AfterReadSingle(primaryResource, ResourcePipeline.GetSingle); - _hookExecutor.OnReturnSingle(primaryResource, ResourcePipeline.GetSingle); - - return primaryResource; + return await GetPrimaryResourceByIdAsync(id, TopFieldSelection.PreserveExisting, cancellationToken); } /// @@ -122,8 +107,6 @@ public virtual async Task GetSecondaryAsync(TId id, string relationshipN AssertHasRelationship(_request.Relationship, relationshipName); - _hookExecutor.BeforeReadSingle(id, ResourcePipeline.GetRelationship); - QueryLayer secondaryLayer = _queryLayerComposer.ComposeFromConstraints(_request.SecondaryResource); QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship); @@ -132,8 +115,6 @@ public virtual async Task GetSecondaryAsync(TId id, string relationshipN TResource primaryResource = primaryResources.SingleOrDefault(); AssertPrimaryResourceExists(primaryResource); - _hookExecutor.AfterReadSingle(primaryResource, ResourcePipeline.GetRelationship); - object secondaryResourceOrResources = _request.Relationship.GetValue(primaryResource); if (secondaryResourceOrResources is ICollection secondaryResources && secondaryLayer.Pagination?.PageSize?.Value == secondaryResources.Count) @@ -141,7 +122,7 @@ public virtual async Task GetSecondaryAsync(TId id, string relationshipN _paginationContext.IsPageFull = true; } - return _hookExecutor.OnReturnRelationship(secondaryResourceOrResources); + return secondaryResourceOrResources; } /// @@ -157,8 +138,6 @@ public virtual async Task GetRelationshipAsync(TId id, string relationsh AssertHasRelationship(_request.Relationship, relationshipName); - _hookExecutor.BeforeReadSingle(id, ResourcePipeline.GetRelationship); - QueryLayer secondaryLayer = _queryLayerComposer.ComposeSecondaryLayerForRelationship(_request.SecondaryResource); QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship); @@ -167,11 +146,7 @@ public virtual async Task GetRelationshipAsync(TId id, string relationsh TResource primaryResource = primaryResources.SingleOrDefault(); AssertPrimaryResourceExists(primaryResource); - _hookExecutor.AfterReadSingle(primaryResource, ResourcePipeline.GetRelationship); - - object secondaryResourceOrResources = _request.Relationship.GetValue(primaryResource); - - return _hookExecutor.OnReturnRelationship(secondaryResourceOrResources); + return _request.Relationship.GetValue(primaryResource); } /// @@ -187,8 +162,6 @@ public virtual async Task CreateAsync(TResource resource, Cancellatio TResource resourceFromRequest = resource; _resourceChangeTracker.SetRequestedAttributeValues(resourceFromRequest); - _hookExecutor.BeforeCreate(resourceFromRequest); - TResource resourceForDatabase = await _repositoryAccessor.GetForCreateAsync(resource.Id, cancellationToken); _resourceChangeTracker.SetInitiallyStoredAttributeValues(resourceForDatabase); @@ -218,19 +191,10 @@ public virtual async Task CreateAsync(TResource resource, Cancellatio TResource resourceFromDatabase = await GetPrimaryResourceByIdAsync(resourceForDatabase.Id, TopFieldSelection.WithAllAttributes, cancellationToken); - _hookExecutor.AfterCreate(resourceFromDatabase); - _resourceChangeTracker.SetFinallyStoredAttributeValues(resourceFromDatabase); bool hasImplicitChanges = _resourceChangeTracker.HasImplicitChanges(); - - if (!hasImplicitChanges) - { - return null; - } - - _hookExecutor.OnReturnSingle(resourceFromDatabase, ResourcePipeline.Post); - return resourceFromDatabase; + return hasImplicitChanges ? resourceFromDatabase : null; } protected virtual async Task InitializeResourceAsync(TResource resourceForDatabase, CancellationToken cancellationToken) @@ -360,8 +324,6 @@ public virtual async Task UpdateAsync(TId id, TResource resource, Can TResource resourceFromRequest = resource; _resourceChangeTracker.SetRequestedAttributeValues(resourceFromRequest); - _hookExecutor.BeforeUpdateResource(resourceFromRequest); - TResource resourceFromDatabase = await GetPrimaryResourceForUpdateAsync(id, cancellationToken); _resourceChangeTracker.SetInitiallyStoredAttributeValues(resourceFromDatabase); @@ -380,19 +342,10 @@ public virtual async Task UpdateAsync(TId id, TResource resource, Can TResource afterResourceFromDatabase = await GetPrimaryResourceByIdAsync(id, TopFieldSelection.WithAllAttributes, cancellationToken); - _hookExecutor.AfterUpdateResource(afterResourceFromDatabase); - _resourceChangeTracker.SetFinallyStoredAttributeValues(afterResourceFromDatabase); bool hasImplicitChanges = _resourceChangeTracker.HasImplicitChanges(); - - if (!hasImplicitChanges) - { - return null; - } - - _hookExecutor.OnReturnSingle(afterResourceFromDatabase, ResourcePipeline.Patch); - return afterResourceFromDatabase; + return hasImplicitChanges ? afterResourceFromDatabase : null; } /// @@ -413,8 +366,6 @@ public virtual async Task SetRelationshipAsync(TId primaryId, string relationshi await _resourceDefinitionAccessor.OnPrepareWriteAsync(resourceFromDatabase, OperationKind.SetRelationship, cancellationToken); - _hookExecutor.BeforeUpdateRelationship(resourceFromDatabase); - try { await _repositoryAccessor.SetRelationshipAsync(resourceFromDatabase, secondaryResourceIds, cancellationToken); @@ -424,8 +375,6 @@ public virtual async Task SetRelationshipAsync(TId primaryId, string relationshi await AssertRightResourcesExistAsync(secondaryResourceIds, cancellationToken); throw; } - - _hookExecutor.AfterUpdateRelationship(resourceFromDatabase); } /// @@ -436,8 +385,6 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke id }); - _hookExecutor.BeforeDelete(id); - try { await _repositoryAccessor.DeleteAsync(id, cancellationToken); @@ -447,8 +394,6 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke _ = await GetPrimaryResourceByIdAsync(id, TopFieldSelection.OnlyIdAttribute, cancellationToken); throw; } - - _hookExecutor.AfterDelete(id); } /// @@ -532,8 +477,8 @@ public class JsonApiResourceService : JsonApiResourceService resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, hookExecutor) + IResourceChangeTracker resourceChangeTracker) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker) { } } diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index a13c6340f5..dcadd46ce2 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -1,6 +1,5 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Repositories; @@ -34,14 +33,12 @@ public ServiceDiscoveryFacadeTests() _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); - _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); - _services.AddScoped(_ => new Mock().Object); _resourceGraphBuilder = new ResourceGraphBuilder(_options, LoggerFactory); } @@ -133,24 +130,5 @@ public void Can_add_resource_definition_from_current_assembly_to_container() var resourceDefinition = services.GetRequiredService>(); resourceDefinition.Should().BeOfType(); } - - [Fact] - public void Can_add_resource_hooks_definition_from_current_assembly_to_container() - { - // Arrange - var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, LoggerFactory); - facade.AddCurrentAssembly(); - - _options.EnableResourceHooks = true; - - // Act - facade.DiscoverInjectables(); - - // Assert - ServiceProvider services = _services.BuildServiceProvider(); - - var resourceHooksDefinition = services.GetRequiredService>(); - resourceHooksDefinition.Should().BeOfType(); - } } } diff --git a/test/DiscoveryTests/TestResourceHooksDefinition.cs b/test/DiscoveryTests/TestResourceHooksDefinition.cs deleted file mode 100644 index 0bfc1bd03b..0000000000 --- a/test/DiscoveryTests/TestResourceHooksDefinition.cs +++ /dev/null @@ -1,15 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; - -namespace DiscoveryTests -{ - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class TestResourceHooksDefinition : ResourceHooksDefinition - { - public TestResourceHooksDefinition(IResourceGraph resourceGraph) - : base(resourceGraph) - { - } - } -} diff --git a/test/DiscoveryTests/TestResourceService.cs b/test/DiscoveryTests/TestResourceService.cs index d3955049cb..d5e6e53d0f 100644 --- a/test/DiscoveryTests/TestResourceService.cs +++ b/test/DiscoveryTests/TestResourceService.cs @@ -1,6 +1,5 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Repositories; @@ -14,9 +13,8 @@ namespace DiscoveryTests public sealed class TestResourceService : JsonApiResourceService { public TestResourceService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, - IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, - IResourceHookExecutorFacade hookExecutor) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, hookExecutor) + IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker) { } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs index 2b63cf8a1e..fcb6af0d6b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Repositories; @@ -21,8 +20,8 @@ public sealed class ConsumerArticleService : JsonApiResourceService resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, hookExecutor) + IResourceChangeTracker resourceChangeTracker) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/MultiTenancy/MultiTenantResourceService.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/MultiTenancy/MultiTenantResourceService.cs index 68a8d78c1f..ba6ad3e96f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/MultiTenancy/MultiTenantResourceService.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/MultiTenancy/MultiTenantResourceService.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Repositories; @@ -24,8 +23,8 @@ public class MultiTenantResourceService : JsonApiResourceService public MultiTenantResourceService(ITenantProvider tenantProvider, IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, hookExecutor) + IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker) { _tenantProvider = tenantProvider; } @@ -91,9 +90,8 @@ public class MultiTenantResourceService : MultiTenantResourceService< { public MultiTenantResourceService(ITenantProvider tenantProvider, IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) - : base(tenantProvider, repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, - hookExecutor) + IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker) + : base(tenantProvider, repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker) { } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/ArticlesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/ArticlesController.cs deleted file mode 100644 index 34f6c30abf..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/ArticlesController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Controllers -{ - public sealed class ArticlesController : JsonApiController
- { - public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/AuthorsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/AuthorsController.cs deleted file mode 100644 index ec06d74d15..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/AuthorsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Controllers -{ - public sealed class AuthorsController : JsonApiController - { - public AuthorsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/PassportsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/PassportsController.cs deleted file mode 100644 index 04d3b67d05..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/PassportsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Controllers -{ - public sealed class PassportsController : JsonApiController - { - public PassportsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/PeopleController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/PeopleController.cs deleted file mode 100644 index b7ffdacd19..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/PeopleController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Controllers -{ - public sealed class PeopleController : JsonApiController - { - public PeopleController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/TodoItemsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/TodoItemsController.cs deleted file mode 100644 index 94e965d69c..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/TodoItemsController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Controllers -{ - public sealed class TodoItemsController : JsonApiController - { - public TodoItemsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/UsersController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/UsersController.cs deleted file mode 100644 index d426354956..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Controllers/UsersController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Controllers -{ - public sealed class UsersController : JsonApiController - { - public UsersController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/ArticleHooksDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/ArticleHooksDefinition.cs deleted file mode 100644 index 11956bc85f..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/ArticleHooksDefinition.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Definitions -{ - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class ArticleHooksDefinition : ResourceHooksDefinition
- { - public ArticleHooksDefinition(IResourceGraph resourceGraph) - : base(resourceGraph) - { - } - - public override IEnumerable
OnReturn(HashSet
resources, ResourcePipeline pipeline) - { - if (pipeline == ResourcePipeline.GetSingle && resources.Any(article => article.Caption == "Classified")) - { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) - { - Title = "You are not allowed to see this article." - }); - } - - return resources.Where(article => article.Caption != "This should not be included").ToArray(); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/LockableHooksDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/LockableHooksDefinition.cs deleted file mode 100644 index 81ae8a2e3c..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/LockableHooksDefinition.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Definitions -{ - public abstract class LockableHooksDefinition : ResourceHooksDefinition - where T : class, IIsLockable, IIdentifiable - { - private readonly IResourceGraph _resourceGraph; - - protected LockableHooksDefinition(IResourceGraph resourceGraph) - : base(resourceGraph) - { - _resourceGraph = resourceGraph; - } - - protected void DisallowLocked(IEnumerable resources) - { - foreach (T resource in resources ?? Enumerable.Empty()) - { - if (resource.IsLocked) - { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) - { - Title = "You are not allowed to update fields or relationships of " + - $"locked resource of type '{_resourceGraph.GetResourceContext().PublicName}'." - }); - } - } - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/PassportHooksDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/PassportHooksDefinition.cs deleted file mode 100644 index 26c1cfdbc3..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/PassportHooksDefinition.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Definitions -{ - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class PassportHooksDefinition : LockableHooksDefinition - { - public PassportHooksDefinition(IResourceGraph resourceGraph) - : base(resourceGraph) - { - } - - public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) - { - if (pipeline == ResourcePipeline.GetSingle && isIncluded) - { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) - { - Title = "You are not allowed to include passports on individual persons." - }); - } - } - - public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) - { - resourcesByRelationship.GetByRelationship().ToList().ForEach(pair => DisallowLocked(pair.Value)); - } - - public override IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline) - { - return resources.Where(passport => !passport.IsLocked).ToArray(); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/PersonHooksDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/PersonHooksDefinition.cs deleted file mode 100644 index bf30f5594c..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/PersonHooksDefinition.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Definitions -{ - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class PersonHooksDefinition : LockableHooksDefinition - { - public PersonHooksDefinition(IResourceGraph resourceGraph) - : base(resourceGraph) - { - } - - public override IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, - ResourcePipeline pipeline) - { - BeforeImplicitUpdateRelationship(resourcesByRelationship, pipeline); - return ids; - } - - public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) - { - resourcesByRelationship.GetByRelationship().ToList().ForEach(pair => DisallowLocked(pair.Value)); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/TagHooksDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/TagHooksDefinition.cs deleted file mode 100644 index 0ea050e3d7..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/TagHooksDefinition.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Definitions -{ - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class TagHooksDefinition : ResourceHooksDefinition - { - public TagHooksDefinition(IResourceGraph resourceGraph) - : base(resourceGraph) - { - } - - public override IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline) - { - return resources.Where(tag => tag.Name != "This should not be included").ToArray(); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/TodoItemHooksDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/TodoItemHooksDefinition.cs deleted file mode 100644 index 09be6b9491..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Definitions/TodoItemHooksDefinition.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Definitions -{ - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class TodoItemHooksDefinition : LockableHooksDefinition - { - public TodoItemHooksDefinition(IResourceGraph resourceGraph) - : base(resourceGraph) - { - } - - public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) - { - if (stringId == "1337") - { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) - { - Title = "You are not allowed to update the author of todo items." - }); - } - } - - public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) - { - List todoItems = resourcesByRelationship.GetByRelationship().SelectMany(pair => pair.Value).ToList(); - DisallowLocked(todoItems); - } - - public override IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline) - { - return resources.Where(todoItem => todoItem.Description != "This should not be included").ToArray(); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/HooksDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/HooksDbContext.cs deleted file mode 100644 index 1c8dd2948d..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/HooksDbContext.cs +++ /dev/null @@ -1,44 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; -using Microsoft.EntityFrameworkCore; - -// @formatter:wrap_chained_method_calls chop_always - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class HooksDbContext : DbContext - { - public DbSet TodoItems { get; set; } - public DbSet People { get; set; } - public DbSet
Articles { get; set; } - public DbSet AuthorDifferentDbContextName { get; set; } - public DbSet Users { get; set; } - - public HooksDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder builder) - { - builder.Entity() - .HasKey(bc => new - { - bc.ArticleId, - bc.TagId - }); - - builder.Entity() - .HasOne(person => person.StakeholderTodoItem) - .WithMany(todoItem => todoItem.Stakeholders) - .OnDelete(DeleteBehavior.Cascade); - - builder.Entity() - .HasOne(passport => passport.Person) - .WithOne(person => person.Passport) - .HasForeignKey("PassportId") - .OnDelete(DeleteBehavior.SetNull); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/HooksFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/HooksFakers.cs deleted file mode 100644 index 165068309d..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/HooksFakers.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using Bogus; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; -using TestBuildingBlocks; -using Person = JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models.Person; - -// @formatter:wrap_chained_method_calls chop_always -// @formatter:keep_existing_linebreaks true - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks -{ - internal sealed class HooksFakers : FakerContainer - { - private readonly Lazy> _lazyAuthorFaker = new Lazy>(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(author => author.Name, faker => faker.Person.FullName)); - - private readonly Lazy> _lazyArticleFaker = new Lazy>(() => - new Faker
() - .UseSeed(GetFakerSeed()) - .RuleFor(article => article.Caption, faker => faker.Lorem.Word()) - .RuleFor(article => article.Url, faker => faker.Internet.Url())); - - private readonly Lazy> _lazyUserFaker = new Lazy>(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(user => user.UserName, faker => faker.Person.UserName) - .RuleFor(user => user.Password, faker => faker.Internet.Password())); - - private readonly Lazy> _lazyTodoItemFaker = new Lazy>(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(todoItem => todoItem.Description, faker => faker.Random.Words())); - - private readonly Lazy> _lazyPersonFaker = new Lazy>(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(person => person.Name, faker => faker.Person.FullName)); - - private readonly Lazy> _lazyTagFaker = new Lazy>(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(tag => tag.Name, faker => faker.Lorem.Word())); - - public Faker Author => _lazyAuthorFaker.Value; - public Faker
Article => _lazyArticleFaker.Value; - public Faker User => _lazyUserFaker.Value; - public Faker TodoItem => _lazyTodoItemFaker.Value; - public Faker Person => _lazyPersonFaker.Value; - public Faker Tag => _lazyTagFaker.Value; - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Article.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Article.cs deleted file mode 100644 index 00391a69ba..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Article.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Article : Identifiable - { - [Attr] - public string Caption { get; set; } - - [Attr] - public string Url { get; set; } - - [NotMapped] - [HasManyThrough(nameof(ArticleTags))] - public ISet Tags { get; set; } - - public ISet ArticleTags { get; set; } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/ArticleTag.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/ArticleTag.cs deleted file mode 100644 index 291b751342..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/ArticleTag.cs +++ /dev/null @@ -1,14 +0,0 @@ -using JetBrains.Annotations; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class ArticleTag - { - public int ArticleId { get; set; } - public Article Article { get; set; } - - public int TagId { get; set; } - public Tag Tag { get; set; } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Author.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Author.cs deleted file mode 100644 index 4bc2f0f781..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Author.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Author : Identifiable - { - [Attr] - public string Name { get; set; } - - [HasMany] - public IList
Articles { get; set; } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/IIsLockable.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/IIsLockable.cs deleted file mode 100644 index 08923b91cd..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/IIsLockable.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models -{ - public interface IIsLockable - { - bool IsLocked { get; } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Passport.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Passport.cs deleted file mode 100644 index 673d1e0d8b..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Passport.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Passport : Identifiable, IIsLockable - { - [Attr] - public bool IsLocked { get; set; } - - [HasOne] - public Person Person { get; set; } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Person.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Person.cs deleted file mode 100644 index fe7d26d92e..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Person.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Person : Identifiable, IIsLockable - { - public bool IsLocked { get; set; } - - [Attr] - public string Name { get; set; } - - [HasMany] - public ISet TodoItems { get; set; } - - [HasOne] - public TodoItem StakeholderTodoItem { get; set; } - - [HasOne] - public Passport Passport { get; set; } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Tag.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Tag.cs deleted file mode 100644 index b70a7779bc..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/Tag.cs +++ /dev/null @@ -1,13 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Tag : Identifiable - { - [Attr] - public string Name { get; set; } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/TodoItem.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/TodoItem.cs deleted file mode 100644 index ee2273c8b2..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/TodoItem.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class TodoItem : Identifiable, IIsLockable - { - public bool IsLocked { get; set; } - - [Attr] - public string Description { get; set; } - - [HasMany] - public ISet Stakeholders { get; set; } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/User.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/User.cs deleted file mode 100644 index 56fa0dcec5..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/Models/User.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class User : Identifiable - { - [Attr] - public string UserName { get; set; } - - [Attr(Capabilities = AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] - public string Password { get; set; } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHookTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHookTests.cs deleted file mode 100644 index 2e7ef5129a..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHookTests.cs +++ /dev/null @@ -1,707 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Client.Internal; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Controllers; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Definitions; -using JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks.Models; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using TestBuildingBlocks; -using Xunit; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks -{ - public sealed class ResourceHookTests : IClassFixture, HooksDbContext>> - { - private readonly ExampleIntegrationTestContext, HooksDbContext> _testContext; - private readonly HooksFakers _fakers = new HooksFakers(); - - public ResourceHookTests(ExampleIntegrationTestContext, HooksDbContext> testContext) - { - _testContext = testContext; - - testContext.UseController(); - testContext.UseController(); - testContext.UseController(); - testContext.UseController(); - testContext.UseController(); - testContext.UseController(); - - testContext.ConfigureServicesAfterStartup(services => - { - services.AddScoped, ArticleHooksDefinition>(); - services.AddScoped, PassportHooksDefinition>(); - services.AddScoped, PersonHooksDefinition>(); - services.AddScoped, TagHooksDefinition>(); - services.AddScoped, TodoItemHooksDefinition>(); - - services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>)); - }); - - var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); - options.DisableTopPagination = false; - options.DisableChildrenPagination = false; - } - - [Fact] - public async Task Can_create_user_with_password() - { - // Arrange - User newUser = _fakers.User.Generate(); - - IRequestSerializer serializer = GetRequestSerializer(user => new - { - user.Password, - user.UserName - }); - - string requestBody = serializer.Serialize(newUser); - - const string route = "/api/v1/users"; - - // Act - (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - - User responseUser = GetResponseDeserializer().DeserializeSingle(responseDocument).Data; - var document = JsonConvert.DeserializeObject(responseDocument); - - document.Should().NotBeNull(); - document!.SingleData.Attributes.Should().NotContainKey("password"); - document.SingleData.Attributes["userName"].Should().Be(newUser.UserName); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - User userInDatabase = await dbContext.Users.FirstWithIdAsync(responseUser.Id); - - userInDatabase.UserName.Should().Be(newUser.UserName); - userInDatabase.Password.Should().Be(newUser.Password); - }); - } - - [Fact] - public async Task Can_update_user_password() - { - // Arrange - User existingUser = _fakers.User.Generate(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.Users.Add(existingUser); - await dbContext.SaveChangesAsync(); - }); - - existingUser.Password = _fakers.User.Generate().Password; - - IRequestSerializer serializer = GetRequestSerializer(user => new - { - user.Password - }); - - string requestBody = serializer.Serialize(existingUser); - - string route = $"/api/v1/users/{existingUser.Id}"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.SingleData.Attributes.Should().NotContainKey("password"); - responseDocument.SingleData.Attributes["userName"].Should().Be(existingUser.UserName); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - User userInDatabase = await dbContext.Users.FirstWithIdAsync(existingUser.Id); - - userInDatabase.Password.Should().Be(existingUser.Password); - }); - } - - [Fact] - public async Task Can_block_access_to_resource_from_GetSingle_endpoint_using_BeforeRead_hook() - { - // Arrange - const string route = "/api/v1/todoItems/1337"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("You are not allowed to update the author of todo items."); - error.Detail.Should().BeNull(); - } - - [Fact] - public async Task Can_block_access_to_included_resource_using_BeforeRead_hook() - { - // Arrange - const string route = "/api/v1/people/1?include=passport"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("You are not allowed to include passports on individual persons."); - error.Detail.Should().BeNull(); - } - - [Fact] - public async Task Can_block_access_to_resource_from_GetSingle_endpoint_using_OnReturn_hook() - { - // Arrange - Article article = _fakers.Article.Generate(); - article.Caption = "Classified"; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.Articles.Add(article); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/api/v1/articles/{article.Id}"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("You are not allowed to see this article."); - error.Detail.Should().BeNull(); - } - - [Fact] - public async Task Can_hide_primary_resource_from_result_set_from_GetAll_endpoint_using_OnReturn_hook() - { - // Arrange - List
articles = _fakers.Article.Generate(3); - const string toBeExcluded = "This should not be included"; - articles[0].Caption = toBeExcluded; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.Articles.AddRange(articles); - await dbContext.SaveChangesAsync(); - }); - - const string route = "/api/v1/articles"; - - // Act - (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Should().NotContain(toBeExcluded); - } - - [Fact] - public async Task Can_hide_secondary_resource_from_ToOne_relationship_using_OnReturn_hook() - { - // Arrange - Person person = _fakers.Person.Generate(); - - person.Passport = new Passport - { - IsLocked = true - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.People.Add(person); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/api/v1/people/{person.Id}/passport"; - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Data.Should().BeNull(); - } - - [Fact] - public async Task Can_hide_secondary_resource_from_ToMany_List_relationship_using_OnReturn_hook() - { - // Arrange - const string toBeExcluded = "This should not be included"; - - Author author = _fakers.Author.Generate(); - author.Articles = _fakers.Article.Generate(3); - author.Articles[0].Caption = toBeExcluded; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.AuthorDifferentDbContextName.Add(author); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/api/v1/authors/{author.Id}/articles"; - - // Act - (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Should().NotContain(toBeExcluded); - } - - [Fact] - public async Task Can_hide_secondary_resource_from_ToMany_Set_relationship_using_OnReturn_hook() - { - // Arrange - const string toBeExcluded = "This should not be included"; - - Person person = _fakers.Person.Generate(); - person.TodoItems = _fakers.TodoItem.Generate(3).ToHashSet(); - person.TodoItems.First().Description = toBeExcluded; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.People.Add(person); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/api/v1/people/{person.Id}/todoItems"; - - // Act - (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Should().NotContain(toBeExcluded); - } - - [Fact] - public async Task Can_hide_resource_from_included_HasManyThrough_relationship_using_OnReturn_hook() - { - // Arrange - const string toBeExcluded = "This should not be included"; - - List tags = _fakers.Tag.Generate(2); - tags[0].Name = toBeExcluded; - - Article article = _fakers.Article.Generate(); - - article.ArticleTags = new HashSet - { - new ArticleTag - { - Tag = tags[0] - }, - new ArticleTag - { - Tag = tags[1] - } - }; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.Articles.Add(article); - await dbContext.SaveChangesAsync(); - }); - - // Workaround for https://github.com/dotnet/efcore/issues/21026 - var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); - options.DisableTopPagination = false; - options.DisableChildrenPagination = true; - - const string route = "/api/v1/articles?include=tags"; - - // Act - (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Should().NotContain(toBeExcluded); - } - - [Fact] - public async Task Can_block_creating_ToOne_relationship_using_BeforeUpdateRelationship_hook() - { - // Arrange - Person lockedPerson = _fakers.Person.Generate(); - lockedPerson.IsLocked = true; - lockedPerson.Passport = new Passport(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.People.Add(lockedPerson); - await dbContext.SaveChangesAsync(); - }); - - var requestBody = new - { - data = new - { - type = "people", - relationships = new - { - passport = new - { - data = new - { - type = "passports", - id = lockedPerson.Passport.StringId - } - } - } - } - }; - - const string route = "/api/v1/people"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'people'."); - error.Detail.Should().BeNull(); - } - - [Fact] - public async Task Can_block_replacing_ToOne_relationship_using_BeforeImplicitUpdateRelationship_hook() - { - // Arrange - Person person = _fakers.Person.Generate(); - - person.Passport = new Passport - { - IsLocked = true - }; - - var newPassport = new Passport(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.AddRange(person, newPassport); - await dbContext.SaveChangesAsync(); - }); - - var requestBody = new - { - data = new - { - type = "people", - id = person.Id, - relationships = new - { - passport = new - { - data = new - { - type = "passports", - id = newPassport.StringId - } - } - } - } - }; - - string route = $"/api/v1/people/{person.Id}"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'passports'."); - error.Detail.Should().BeNull(); - } - - [Fact] - public async Task Can_block_clearing_ToOne_relationship_using_BeforeImplicitUpdateRelationship_hook() - { - // Arrange - Person person = _fakers.Person.Generate(); - - person.Passport = new Passport - { - IsLocked = true - }; - - var newPassport = new Passport(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.AddRange(person, newPassport); - await dbContext.SaveChangesAsync(); - }); - - var requestBody = new - { - data = new - { - type = "people", - id = person.Id, - relationships = new - { - passport = new - { - data = (object)null - } - } - } - }; - - string route = $"/api/v1/people/{person.Id}"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'passports'."); - error.Detail.Should().BeNull(); - } - - [Fact] - public async Task Can_block_deleting_primary_resource_using_BeforeImplicitUpdateRelationship_hook() - { - // Arrange - Person lockedPerson = _fakers.Person.Generate(); - lockedPerson.IsLocked = true; - lockedPerson.Passport = new Passport(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.People.Add(lockedPerson); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/api/v1/passports/{lockedPerson.Passport.StringId}"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'people'."); - error.Detail.Should().BeNull(); - } - - [Fact] - public async Task Can_block_creating_ToMany_relationship_using_BeforeUpdateRelationship_hook() - { - // Arrange - List persons = _fakers.Person.Generate(2); - TodoItem lockedTodo = _fakers.TodoItem.Generate(); - lockedTodo.IsLocked = true; - lockedTodo.Stakeholders = persons.ToHashSet(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.TodoItems.Add(lockedTodo); - await dbContext.SaveChangesAsync(); - }); - - var requestBody = new - { - data = new - { - type = "todoItems", - relationships = new - { - stakeholders = new - { - data = new[] - { - new - { - type = "people", - id = persons[0].StringId - }, - new - { - type = "people", - id = persons[1].StringId - } - } - } - } - } - }; - - const string route = "/api/v1/todoItems"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'todoItems'."); - error.Detail.Should().BeNull(); - } - - [Fact] - public async Task Can_block_replacing_ToMany_relationship_using_BeforeImplicitUpdateRelationship_hook() - { - // Arrange - List persons = _fakers.Person.Generate(2); - - TodoItem lockedTodo = _fakers.TodoItem.Generate(); - lockedTodo.IsLocked = true; - lockedTodo.Stakeholders = persons.ToHashSet(); - - TodoItem unlockedTodo = _fakers.TodoItem.Generate(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.TodoItems.AddRange(lockedTodo, unlockedTodo); - await dbContext.SaveChangesAsync(); - }); - - var requestBody = new - { - data = new - { - type = "todoItems", - id = unlockedTodo.Id, - relationships = new - { - stakeholders = new - { - data = new[] - { - new - { - type = "people", - id = persons[0].StringId - }, - new - { - type = "people", - id = persons[1].StringId - } - } - } - } - } - }; - - string route = $"/api/v1/todoItems/{unlockedTodo.Id}"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'todoItems'."); - error.Detail.Should().BeNull(); - } - - [Fact] - public async Task Can_block_clearing_ToMany_relationship_using_BeforeImplicitUpdateRelationship_hook() - { - // Arrange - List persons = _fakers.Person.Generate(2); - TodoItem lockedTodo = _fakers.TodoItem.Generate(); - lockedTodo.IsLocked = true; - lockedTodo.Stakeholders = persons.ToHashSet(); - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.TodoItems.Add(lockedTodo); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/api/v1/people/{persons[0].Id}"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.Forbidden); - error.Title.Should().Be("You are not allowed to update fields or relationships of locked resource of type 'todoItems'."); - error.Detail.Should().BeNull(); - } - - private IRequestSerializer GetRequestSerializer(Expression> attributes = null, - Expression> relationships = null) - where TResource : class, IIdentifiable - { - var graph = _testContext.Factory.Services.GetRequiredService(); - - var serializer = _testContext.Factory.Services.GetRequiredService(); - serializer.AttributesToSerialize = attributes != null ? graph.GetAttributes(attributes) : null; - serializer.RelationshipsToSerialize = relationships != null ? graph.GetRelationships(relationships) : null; - return serializer; - } - - private IResponseDeserializer GetResponseDeserializer() - { - return _testContext.Factory.Services.GetRequiredService(); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHooksStartup.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHooksStartup.cs deleted file mode 100644 index 51f0a08c51..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceHooks/ResourceHooksStartup.cs +++ /dev/null @@ -1,28 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCoreExampleTests.Startups; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceHooks -{ - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class ResourceHooksStartup : TestableStartup - where TDbContext : DbContext - { - public override void ConfigureServices(IServiceCollection services) - { - base.ConfigureServices(services); - services.AddClientSerialization(); - } - - protected override void SetJsonApiOptions(JsonApiOptions options) - { - base.SetJsonApiOptions(options); - - options.Namespace = "api/v1"; - options.EnableResourceHooks = true; - options.LoadDatabaseValues = true; - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionAwareResourceService.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionAwareResourceService.cs index 74a7e9c8b6..002f8df2b1 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionAwareResourceService.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionAwareResourceService.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Repositories; @@ -27,8 +26,8 @@ public class SoftDeletionAwareResourceService : JsonApiResourceS public SoftDeletionAwareResourceService(ISystemClock systemClock, ITargetedFields targetedFields, IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, hookExecutor) + IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker) { _systemClock = systemClock; _targetedFields = targetedFields; @@ -120,9 +119,9 @@ public class SoftDeletionAwareResourceService : SoftDeletionAwareReso { public SoftDeletionAwareResourceService(ISystemClock systemClock, ITargetedFields targetedFields, IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) + IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker) : base(systemClock, targetedFields, repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, - resourceChangeTracker, hookExecutor) + resourceChangeTracker) { } } diff --git a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index 7bb63532a5..bd94ee6c7c 100644 --- a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -11,12 +11,9 @@ using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; -using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -25,40 +22,6 @@ namespace UnitTests.Extensions { public sealed class ServiceCollectionExtensionsTests { - [Fact] - public void AddJsonApiInternals_Adds_All_Required_Services() - { - // Arrange - var services = new ServiceCollection(); - services.AddLogging(); - services.AddDbContext(options => options.UseInMemoryDatabase("UnitTestDb")); - services.AddJsonApi(); - - // Act - // this is required because the DbContextResolver requires access to the current HttpContext - // to get the request scoped DbContext instance - services.AddScoped(); - ServiceProvider provider = services.BuildServiceProvider(); - - // Assert - var request = provider.GetRequiredService() as JsonApiRequest; - Assert.NotNull(request); - var resourceGraph = provider.GetService(); - Assert.NotNull(resourceGraph); - request.PrimaryResource = resourceGraph.GetResourceContext(); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService(typeof(IResourceRepository))); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - } - [Fact] public void RegisterResource_DeviatingDbContextPropertyName_RegistersCorrectly() { diff --git a/test/UnitTests/ResourceHooks/DiscoveryTests.cs b/test/UnitTests/ResourceHooks/DiscoveryTests.cs deleted file mode 100644 index a561f3278c..0000000000 --- a/test/UnitTests/ResourceHooks/DiscoveryTests.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; -using Xunit; - -namespace UnitTests.ResourceHooks -{ - public sealed class DiscoveryTests - { - [Fact] - public void HookDiscovery_StandardResourceDefinition_CanDiscover() - { - // Act - var hookConfig = new HooksDiscovery(MockProvider(new DummyResourceDefinition())); - - // Assert - Assert.Contains(ResourceHook.BeforeDelete, hookConfig.ImplementedHooks); - Assert.Contains(ResourceHook.AfterDelete, hookConfig.ImplementedHooks); - } - - [Fact] - public void HookDiscovery_InheritanceSubclass_CanDiscover() - { - // Act - var hookConfig = new HooksDiscovery(MockProvider(new AnotherDummyResourceDefinition())); - - // Assert - Assert.Contains(ResourceHook.BeforeDelete, hookConfig.ImplementedHooks); - Assert.Contains(ResourceHook.AfterDelete, hookConfig.ImplementedHooks); - } - - [Fact] - public void HookDiscovery_WronglyUsedLoadDatabaseValueAttribute_ThrowsJsonApiSetupException() - { - // Act - Action action = () => _ = new HooksDiscovery(MockProvider(new YetAnotherDummyResourceDefinition())); - - // Assert - Assert.Throws(action); - } - - [Fact] - public void HookDiscovery_InheritanceWithGenericSubclass_CanDiscover() - { - // Act - var hookConfig = new HooksDiscovery(MockProvider(new GenericDummyResourceDefinition())); - - // Assert - Assert.Contains(ResourceHook.BeforeDelete, hookConfig.ImplementedHooks); - Assert.Contains(ResourceHook.AfterDelete, hookConfig.ImplementedHooks); - } - - private IServiceProvider MockProvider(object service) - where TResource : class, IIdentifiable - { - var services = new ServiceCollection(); - services.AddScoped(_ => (ResourceHooksDefinition)service); - return services.BuildServiceProvider(); - } - - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Dummy : Identifiable - { - [Attr] - public string Unused { get; set; } - } - - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class DummyResourceDefinition : ResourceHooksDefinition - { - public DummyResourceDefinition() - : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) - { - } - - public override IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) - { - return resources; - } - - public override void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) - { - } - } - - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class AnotherDummy : Identifiable - { - [Attr] - public string Unused { get; set; } - } - - public abstract class ResourceDefinitionBase : ResourceHooksDefinition - where T : class, IIdentifiable - { - protected ResourceDefinitionBase(IResourceGraph resourceGraph) - : base(resourceGraph) - { - } - - public override IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) - { - return resources; - } - - public override void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) - { - } - } - - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class AnotherDummyResourceDefinition : ResourceDefinitionBase - { - public AnotherDummyResourceDefinition() - : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) - { - } - } - - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class YetAnotherDummy : Identifiable - { - [Attr] - public string Unused { get; set; } - } - - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class YetAnotherDummyResourceDefinition : ResourceHooksDefinition - { - public YetAnotherDummyResourceDefinition() - : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) - { - } - - public override IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) - { - return resources; - } - - [LoadDatabaseValues(false)] - public override void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) - { - } - } - - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class GenericDummyResourceDefinition : ResourceHooksDefinition - where TResource : class, IIdentifiable - { - public GenericDummyResourceDefinition() - : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) - { - } - - public override IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) - { - return resources; - } - - public override void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) - { - } - } - } -} diff --git a/test/UnitTests/ResourceHooks/Dummy.cs b/test/UnitTests/ResourceHooks/Dummy.cs deleted file mode 100644 index 7768e0bbe8..0000000000 --- a/test/UnitTests/ResourceHooks/Dummy.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace UnitTests.ResourceHooks -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Dummy : Identifiable - { - public string SomeUpdatedProperty { get; set; } - public string SomeNotUpdatedProperty { get; set; } - - [HasOne] - public ToOne FirstToOne { get; set; } - - [HasOne] - public ToOne SecondToOne { get; set; } - - [HasMany] - public ISet ToManies { get; set; } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/Create/AfterCreateTests.cs b/test/UnitTests/ResourceHooks/Executor/Create/AfterCreateTests.cs deleted file mode 100644 index 407d953361..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/Create/AfterCreateTests.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor.Create -{ - public sealed class AfterCreateTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.AfterCreate, - ResourceHook.AfterUpdateRelationship - }; - - [Fact] - public void AfterCreate() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.AfterCreate(todoList, ResourcePipeline.Post); - - // Assert - todoResourceMock.Verify(rd => rd.AfterCreate(It.IsAny>(), ResourcePipeline.Post), Times.Once()); - ownerResourceMock.Verify(rd => rd.AfterUpdateRelationship(It.IsAny>(), ResourcePipeline.Post), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void AfterCreate_Without_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.AfterCreate(todoList, ResourcePipeline.Post); - - // Assert - ownerResourceMock.Verify(rd => rd.AfterUpdateRelationship(It.IsAny>(), ResourcePipeline.Post), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void AfterCreate_Without_Child_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.AfterCreate(todoList, ResourcePipeline.Post); - - // Assert - todoResourceMock.Verify(rd => rd.AfterCreate(It.IsAny>(), ResourcePipeline.Post), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void AfterCreate_Without_Any_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.AfterCreate(todoList, ResourcePipeline.Post); - - // Assert - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/Create/BeforeCreateTests.cs b/test/UnitTests/ResourceHooks/Executor/Create/BeforeCreateTests.cs deleted file mode 100644 index 4d09fa64de..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/Create/BeforeCreateTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor.Create -{ - public sealed class BeforeCreateTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.BeforeCreate, - ResourceHook.BeforeUpdateRelationship - }; - - [Fact] - public void BeforeCreate() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // Assert - todoResourceMock.Verify(rd => rd.BeforeCreate(It.IsAny>(), ResourcePipeline.Post), Times.Once()); - - ownerResourceMock.Verify( - rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), ResourcePipeline.Post), - Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeCreate_Without_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // Assert - todoResourceMock.Verify(rd => rd.BeforeCreate(It.IsAny>(), ResourcePipeline.Post), Times.Never()); - - ownerResourceMock.Verify( - rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), ResourcePipeline.Post), - Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeCreate_Without_Child_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // Assert - todoResourceMock.Verify(rd => rd.BeforeCreate(It.IsAny>(), ResourcePipeline.Post), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeCreate_Without_Any_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // Assert - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/Create/BeforeCreateWithDbValuesTests.cs b/test/UnitTests/ResourceHooks/Executor/Create/BeforeCreateWithDbValuesTests.cs deleted file mode 100644 index deb4e3040f..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/Create/BeforeCreateWithDbValuesTests.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Microsoft.EntityFrameworkCore; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor.Create -{ - public sealed class BeforeCreateWithDbValuesTests : HooksTestsSetup - { - private const string Description = "DESCRIPTION"; - private const string Name = "NAME"; - - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.BeforeCreate, - ResourceHook.BeforeImplicitUpdateRelationship, - ResourceHook.BeforeUpdateRelationship - }; - - private readonly ResourceHook[] _targetHooksNoImplicit = - { - ResourceHook.BeforeCreate, - ResourceHook.BeforeUpdateRelationship - }; - - private readonly string _personId; - private readonly IList _todoList; - private readonly DbContextOptions _options; - - public BeforeCreateWithDbValuesTests() - { - _todoList = CreateTodoWithToOnePerson(); - - _todoList[0].Id = 0; - _todoList[0].Description = Description; - Person person = _todoList[0].OneToOnePerson; - person.Name = Name; - _personId = person.Id.ToString(); - TodoItem implicitTodo = TodoFaker.Generate(); - implicitTodo.Id += 1000; - implicitTodo.OneToOnePerson = person; - implicitTodo.Description = Description + Description; - - _options = InitInMemoryDb(context => - { - context.Set().Add(person); - context.Set().Add(implicitTodo); - context.SaveChanges(); - }); - - _todoList[0].OneToOnePerson = person; - } - - [Fact] - public void BeforeCreate() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - // Act - hookExecutor.BeforeCreate(_todoList, ResourcePipeline.Post); - - // Assert - todoResourceMock.Verify( - rd => rd.BeforeCreate(It.Is>(resources => TodoCheck(resources, Description)), ResourcePipeline.Post), Times.Once()); - - ownerResourceMock.Verify( - rd => rd.BeforeUpdateRelationship(It.Is>(ids => PersonIdCheck(ids, _personId)), It.IsAny>(), - ResourcePipeline.Post), Times.Once()); - - todoResourceMock.Verify( - rd => rd.BeforeImplicitUpdateRelationship( - It.Is>(rh => TodoCheckRelationships(rh, Description + Description)), ResourcePipeline.Post), - Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeCreate_Without_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - // Act - hookExecutor.BeforeCreate(_todoList, ResourcePipeline.Post); - - // Assert - ownerResourceMock.Verify( - rd => rd.BeforeUpdateRelationship(It.Is>(ids => PersonIdCheck(ids, _personId)), It.IsAny>(), - ResourcePipeline.Post), Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeCreate_Without_Child_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - // Act - hookExecutor.BeforeCreate(_todoList, ResourcePipeline.Post); - - // Assert - todoResourceMock.Verify( - rd => rd.BeforeCreate(It.Is>(resources => TodoCheck(resources, Description)), ResourcePipeline.Post), Times.Once()); - - todoResourceMock.Verify( - rd => rd.BeforeImplicitUpdateRelationship( - It.Is>(rh => TodoCheckRelationships(rh, Description + Description)), ResourcePipeline.Post), - Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeCreate_NoImplicit() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooksNoImplicit, ResourceHook.BeforeUpdate); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooksNoImplicit, ResourceHook.BeforeUpdateRelationship); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - // Act - hookExecutor.BeforeCreate(_todoList, ResourcePipeline.Post); - - // Assert - todoResourceMock.Verify( - rd => rd.BeforeCreate(It.Is>(resources => TodoCheck(resources, Description)), ResourcePipeline.Post), Times.Once()); - - ownerResourceMock.Verify( - rd => rd.BeforeUpdateRelationship(It.Is>(ids => PersonIdCheck(ids, _personId)), It.IsAny>(), - ResourcePipeline.Post), Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeCreate_NoImplicit_Without_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooksNoImplicit, ResourceHook.BeforeUpdateRelationship); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - // Act - hookExecutor.BeforeCreate(_todoList, ResourcePipeline.Post); - - // Assert - ownerResourceMock.Verify( - rd => rd.BeforeUpdateRelationship(It.Is>(ids => PersonIdCheck(ids, _personId)), It.IsAny>(), - ResourcePipeline.Post), Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeCreate_NoImplicit_Without_Child_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooksNoImplicit, ResourceHook.BeforeUpdate); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - // Act - hookExecutor.BeforeCreate(_todoList, ResourcePipeline.Post); - - // Assert - todoResourceMock.Verify( - rd => rd.BeforeCreate(It.Is>(resources => TodoCheck(resources, Description)), ResourcePipeline.Post), Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - private bool TodoCheck(IEnumerable resources, string checksum) - { - return resources.Single().Description == checksum; - } - - private bool TodoCheckRelationships(IRelationshipsDictionary rh, string checksum) - { - return rh.GetByRelationship().Single().Value.First().Description == checksum; - } - - private bool PersonIdCheck(IEnumerable ids, string checksum) - { - return ids.Single() == checksum; - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/Delete/AfterDeleteTests.cs b/test/UnitTests/ResourceHooks/Executor/Delete/AfterDeleteTests.cs deleted file mode 100644 index 0cf0611b50..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/Delete/AfterDeleteTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor.Delete -{ - public sealed class AfterDeleteTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.AfterDelete - }; - - [Fact] - public void AfterDelete() - { - // Arrange - IHooksDiscovery discovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - (IResourceHookExecutor hookExecutor, Mock> resourceDefinitionMock) = CreateTestObjects(discovery); - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.AfterDelete(todoList, ResourcePipeline.Delete, It.IsAny()); - - // Assert - resourceDefinitionMock.Verify(rd => rd.AfterDelete(It.IsAny>(), ResourcePipeline.Delete, It.IsAny()), Times.Once()); - VerifyNoOtherCalls(resourceDefinitionMock); - } - - [Fact] - public void AfterDelete_Without_Any_Hook_Implemented() - { - // Arrange - IHooksDiscovery discovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (IResourceHookExecutor hookExecutor, Mock> resourceDefinitionMock) = CreateTestObjects(discovery); - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.AfterDelete(todoList, ResourcePipeline.Delete, It.IsAny()); - - // Assert - VerifyNoOtherCalls(resourceDefinitionMock); - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/Delete/BeforeDeleteTests.cs b/test/UnitTests/ResourceHooks/Executor/Delete/BeforeDeleteTests.cs deleted file mode 100644 index 9b2eee151e..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/Delete/BeforeDeleteTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor.Delete -{ - public sealed class BeforeDeleteTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.BeforeDelete - }; - - [Fact] - public void BeforeDelete() - { - // Arrange - IHooksDiscovery discovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - (IResourceHookExecutor hookExecutor, Mock> resourceDefinitionMock) = CreateTestObjects(discovery); - - IEnumerable todoList = CreateTodoWithOwner(); - // Act - hookExecutor.BeforeDelete(todoList, ResourcePipeline.Delete); - - // Assert - resourceDefinitionMock.Verify(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny()), Times.Once()); - resourceDefinitionMock.VerifyNoOtherCalls(); - } - - [Fact] - public void BeforeDelete_Without_Any_Hook_Implemented() - { - // Arrange - IHooksDiscovery discovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (IResourceHookExecutor hookExecutor, Mock> resourceDefinitionMock) = CreateTestObjects(discovery); - - IEnumerable todoList = CreateTodoWithOwner(); - // Act - hookExecutor.BeforeDelete(todoList, ResourcePipeline.Delete); - - // Assert - resourceDefinitionMock.VerifyNoOtherCalls(); - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/Delete/BeforeDeleteWithDbValuesTests.cs b/test/UnitTests/ResourceHooks/Executor/Delete/BeforeDeleteWithDbValuesTests.cs deleted file mode 100644 index 8c6aaf9ceb..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/Delete/BeforeDeleteWithDbValuesTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources.Annotations; -using Microsoft.EntityFrameworkCore; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor.Delete -{ - public sealed class BeforeDeleteWithDbValuesTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.BeforeDelete, - ResourceHook.BeforeImplicitUpdateRelationship, - ResourceHook.BeforeUpdateRelationship - }; - - private readonly DbContextOptions _options; - private readonly Person _person; - - public BeforeDeleteWithDbValuesTests() - { - _person = PersonFaker.Generate(); - TodoItem todo1 = TodoFaker.Generate(); - TodoItem todo2 = TodoFaker.Generate(); - Passport passport = PassportFaker.Generate(); - - _person.Passport = passport; - - _person.TodoItems = new HashSet - { - todo1 - }; - - _person.StakeholderTodoItem = todo2; - - _options = InitInMemoryDb(context => - { - context.Set().Add(_person); - context.SaveChanges(); - }); - } - - [Fact] - public void BeforeDelete() - { - // Arrange - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - IHooksDiscovery passportDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> personResourceMock, - Mock> todoResourceMock, Mock> passportResourceMock) = - CreateTestObjectsC(personDiscovery, todoDiscovery, passportDiscovery, _options); - - // Act - hookExecutor.BeforeDelete(_person.AsList(), ResourcePipeline.Delete); - - // Assert - personResourceMock.Verify(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny()), Times.Once()); - - todoResourceMock.Verify( - rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitTodoItems(rh)), ResourcePipeline.Delete), - Times.Once()); - - passportResourceMock.Verify( - rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitPassports(rh)), ResourcePipeline.Delete), - Times.Once()); - - VerifyNoOtherCalls(personResourceMock, todoResourceMock, passportResourceMock); - } - - [Fact] - public void BeforeDelete_No_Parent_Hooks() - { - // Arrange - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - IHooksDiscovery passportDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> personResourceMock, - Mock> todoResourceMock, Mock> passportResourceMock) = - CreateTestObjectsC(personDiscovery, todoDiscovery, passportDiscovery, _options); - - // Act - hookExecutor.BeforeDelete(_person.AsList(), ResourcePipeline.Delete); - - // Assert - todoResourceMock.Verify( - rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitTodoItems(rh)), ResourcePipeline.Delete), - Times.Once()); - - passportResourceMock.Verify( - rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitPassports(rh)), ResourcePipeline.Delete), - Times.Once()); - - VerifyNoOtherCalls(personResourceMock, todoResourceMock, passportResourceMock); - } - - [Fact] - public void BeforeDelete_No_Children_Hooks() - { - // Arrange - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery passportDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> personResourceMock, - Mock> todoResourceMock, Mock> passportResourceMock) = - CreateTestObjectsC(personDiscovery, todoDiscovery, passportDiscovery, _options); - - // Act - hookExecutor.BeforeDelete(_person.AsList(), ResourcePipeline.Delete); - - // Assert - personResourceMock.Verify(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny()), Times.Once()); - VerifyNoOtherCalls(personResourceMock, todoResourceMock, passportResourceMock); - } - - private bool CheckImplicitTodoItems(IRelationshipsDictionary rh) - { - IDictionary> todoItems = rh.GetByRelationship(); - return todoItems.Count == 2; - } - - private bool CheckImplicitPassports(IRelationshipsDictionary rh) - { - HashSet passports = rh.GetByRelationship().Single().Value; - return passports.Count == 1; - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/IdentifiableManyToManyOnReturnTests.cs b/test/UnitTests/ResourceHooks/Executor/IdentifiableManyToManyOnReturnTests.cs deleted file mode 100644 index 866e0e3d2e..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/IdentifiableManyToManyOnReturnTests.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor -{ - public sealed class IdentifiableManyToManyOnReturnTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.OnReturn - }; - - [Fact] - public void OnReturn() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, List joins, List tags) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.OnReturn(articles, ResourcePipeline.Get); - - // Assert - articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); - - joinResourceMock.Verify( - rd => rd.OnReturn(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.Get), Times.Once()); - - tagResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - - [Fact] - public void OnReturn_GetRelationship() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, List joins, List tags) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.OnReturn(articles, ResourcePipeline.GetRelationship); - - // Assert - articleResourceMock.Verify( - rd => rd.OnReturn(It.Is>(collection => !collection.Except(articles).Any()), ResourcePipeline.GetRelationship), Times.Once()); - - joinResourceMock.Verify( - rd => rd.OnReturn(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.GetRelationship), - Times.Once()); - - tagResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.GetRelationship), - Times.Once()); - - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - - [Fact] - public void OnReturn_Without_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, List joins, List tags) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.OnReturn(articles, ResourcePipeline.Get); - - // Assert - joinResourceMock.Verify( - rd => rd.OnReturn(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.Get), Times.Once()); - - tagResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - - [Fact] - public void OnReturn_Without_Children_Hooks_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, _, List tags) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.OnReturn(articles, ResourcePipeline.Get); - - // Assert - articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); - tagResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - - [Fact] - public void OnReturn_Without_Grand_Children_Hooks_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, List joins, _) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.OnReturn(articles, ResourcePipeline.Get); - - // Assert - articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); - - joinResourceMock.Verify( - rd => rd.OnReturn(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.Get), Times.Once()); - - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - - [Fact] - public void OnReturn_Without_Any_Descendant_Hooks_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, _, _) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.OnReturn(articles, ResourcePipeline.Get); - - // Assert - articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - - [Fact] - public void OnReturn_Without_Any_Hook_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, _, _) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.OnReturn(articles, ResourcePipeline.Get); - - // Assert - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/ManyToManyOnReturnTests.cs b/test/UnitTests/ResourceHooks/Executor/ManyToManyOnReturnTests.cs deleted file mode 100644 index 098a99123a..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/ManyToManyOnReturnTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor -{ - public sealed class ManyToManyOnReturnTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.OnReturn - }; - - [Fact] - public void OnReturn() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - - (List
articles, List tags) = CreateDummyData(); - - // Act - hookExecutor.OnReturn(articles, ResourcePipeline.Get); - - // Assert - articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); - tagResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); - VerifyNoOtherCalls(articleResourceMock, tagResourceMock); - } - - [Fact] - public void OnReturn_Without_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - - (List
articles, List tags) = CreateDummyData(); - - // Act - hookExecutor.OnReturn(articles, ResourcePipeline.Get); - - // Assert - tagResourceMock.Verify(rd => rd.OnReturn(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); - VerifyNoOtherCalls(articleResourceMock, tagResourceMock); - } - - [Fact] - public void OnReturn_Without_Children_Hooks_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - - (List
articles, _) = CreateDummyData(); - - // Act - hookExecutor.OnReturn(articles, ResourcePipeline.Get); - - // Assert - articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); - VerifyNoOtherCalls(articleResourceMock, tagResourceMock); - } - - [Fact] - public void OnReturn_Without_Any_Hook_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - - (List
articles, _) = CreateDummyData(); - - // Act - hookExecutor.OnReturn(articles, ResourcePipeline.Get); - - // Assert - VerifyNoOtherCalls(articleResourceMock, tagResourceMock); - } - - private (List
, List) CreateDummyData() - { - List tagsSubset = TagFaker.Generate(3); - List joinsSubSet = ArticleTagFaker.Generate(3); - Article articleTagsSubset = ArticleFaker.Generate(); - articleTagsSubset.ArticleTags = joinsSubSet.ToHashSet(); - - for (int index = 0; index < 3; index++) - { - joinsSubSet[index].Article = articleTagsSubset; - joinsSubSet[index].Tag = tagsSubset[index]; - } - - List allTags = TagFaker.Generate(3).Concat(tagsSubset).ToList(); - List completeJoin = ArticleTagFaker.Generate(6); - - Article articleWithAllTags = ArticleFaker.Generate(); - articleWithAllTags.ArticleTags = completeJoin.ToHashSet(); - - for (int index = 0; index < 6; index++) - { - completeJoin[index].Article = articleWithAllTags; - completeJoin[index].Tag = allTags[index]; - } - - List
articles = ArrayFactory.Create(articleTagsSubset, articleWithAllTags).ToList(); - return (articles, allTags); - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/Read/BeforeReadTests.cs b/test/UnitTests/ResourceHooks/Executor/Read/BeforeReadTests.cs deleted file mode 100644 index f2e078e4dc..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/Read/BeforeReadTests.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Queries; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor.Read -{ - public sealed class BeforeReadTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.BeforeRead - }; - - [Fact] - public void BeforeRead() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - (IResourceHookExecutor hookExecutor, Mock> todoResourceMock) = CreateTestObjects(todoDiscovery); - - // Act - hookExecutor.BeforeRead(ResourcePipeline.Get); - - // Assert - todoResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, false, null), Times.Once()); - VerifyNoOtherCalls(todoResourceMock); - } - - [Fact] - public void BeforeReadWithInclusion() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (Mock> constraintsMock, _, IResourceHookExecutor hookExecutor, - Mock> todoResourceMock, Mock> ownerResourceMock) = - CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable constraintProviders = Wrap(ToIncludeExpression("owner", "assignee", "stakeholders")); - constraintsMock.Setup(providers => providers.GetEnumerator()).Returns(constraintProviders.GetEnumerator()); - - // Act - hookExecutor.BeforeRead(ResourcePipeline.Get); - - // Assert - todoResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, false, null), Times.Once()); - ownerResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeReadWithNestedInclusion() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery passportDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (Mock> constraintsMock, IResourceHookExecutor hookExecutor, - Mock> todoResourceMock, Mock> ownerResourceMock, - Mock> passportResourceMock) = CreateTestObjectsC(todoDiscovery, personDiscovery, passportDiscovery); - - IEnumerable constraintProviders = Wrap(ToIncludeExpression("owner.passport", "assignee", "stakeholders")); - constraintsMock.Setup(providers => providers.GetEnumerator()).Returns(constraintProviders.GetEnumerator()); - - // Act - hookExecutor.BeforeRead(ResourcePipeline.Get); - - // Assert - todoResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, false, null), Times.Once()); - ownerResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); - passportResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock, passportResourceMock); - } - - [Fact] - public void BeforeReadWithNestedInclusion_No_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery passportDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (Mock> constraintsMock, IResourceHookExecutor hookExecutor, - Mock> todoResourceMock, Mock> ownerResourceMock, - Mock> passportResourceMock) = CreateTestObjectsC(todoDiscovery, personDiscovery, passportDiscovery); - - IEnumerable constraintProviders = Wrap(ToIncludeExpression("owner.passport", "assignee", "stakeholders")); - constraintsMock.Setup(providers => providers.GetEnumerator()).Returns(constraintProviders.GetEnumerator()); - - // Act - hookExecutor.BeforeRead(ResourcePipeline.Get); - - // Assert - ownerResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); - passportResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock, passportResourceMock); - } - - [Fact] - public void BeforeReadWithNestedInclusion_No_Child_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery passportDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (Mock> constraintsMock, IResourceHookExecutor hookExecutor, - Mock> todoResourceMock, Mock> ownerResourceMock, - Mock> passportResourceMock) = CreateTestObjectsC(todoDiscovery, personDiscovery, passportDiscovery); - - IEnumerable constraintProviders = Wrap(ToIncludeExpression("owner.passport", "assignee", "stakeholders")); - constraintsMock.Setup(providers => providers.GetEnumerator()).Returns(constraintProviders.GetEnumerator()); - - // Act - hookExecutor.BeforeRead(ResourcePipeline.Get); - - // Assert - todoResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, false, null), Times.Once()); - passportResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock, passportResourceMock); - } - - [Fact] - public void BeforeReadWithNestedInclusion_No_Grandchild_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery passportDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (Mock> constraintsMock, IResourceHookExecutor hookExecutor, - Mock> todoResourceMock, Mock> ownerResourceMock, - Mock> passportResourceMock) = CreateTestObjectsC(todoDiscovery, personDiscovery, passportDiscovery); - - IEnumerable constraintProviders = Wrap(ToIncludeExpression("owner.passport", "assignee", "stakeholders")); - constraintsMock.Setup(providers => providers.GetEnumerator()).Returns(constraintProviders.GetEnumerator()); - - // Act - hookExecutor.BeforeRead(ResourcePipeline.Get); - - // Assert - todoResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, false, null), Times.Once()); - ownerResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock, passportResourceMock); - } - - [Fact] - public void BeforeReadWithNestedInclusion_Without_Any_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery passportDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (Mock> constraintsMock, IResourceHookExecutor hookExecutor, - Mock> todoResourceMock, Mock> ownerResourceMock, - Mock> passportResourceMock) = CreateTestObjectsC(todoDiscovery, personDiscovery, passportDiscovery); - - IEnumerable constraintProviders = Wrap(ToIncludeExpression("owner.passport", "assignee", "stakeholders")); - constraintsMock.Setup(providers => providers.GetEnumerator()).Returns(constraintProviders.GetEnumerator()); - - // Act - hookExecutor.BeforeRead(ResourcePipeline.Get); - - // Assert - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock, passportResourceMock); - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/Read/IdentifiableManyToManyAfterReadTests.cs b/test/UnitTests/ResourceHooks/Executor/Read/IdentifiableManyToManyAfterReadTests.cs deleted file mode 100644 index a10ab13452..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/Read/IdentifiableManyToManyAfterReadTests.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor.Read -{ - public sealed class IdentifiableManyToManyAfterReadTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.AfterRead - }; - - [Fact] - public void AfterRead() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, List joins, List tags) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.AfterRead(articles, ResourcePipeline.Get); - - // Assert - articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); - - joinResourceMock.Verify( - rd => rd.AfterRead(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.Get, true), - Times.Once()); - - tagResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get, true), - Times.Once()); - - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - - [Fact] - public void AfterRead_Without_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, List joins, List tags) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.AfterRead(articles, ResourcePipeline.Get); - - // Assert - joinResourceMock.Verify( - rd => rd.AfterRead(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.Get, true), - Times.Once()); - - tagResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get, true), - Times.Once()); - - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - - [Fact] - public void AfterRead_Without_Children_Hooks_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, _, List tags) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.AfterRead(articles, ResourcePipeline.Get); - - // Assert - articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); - - tagResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get, true), - Times.Once()); - - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - - [Fact] - public void AfterRead_Without_Grand_Children_Hooks_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, List joins, _) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.AfterRead(articles, ResourcePipeline.Get); - - // Assert - articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); - - joinResourceMock.Verify( - rd => rd.AfterRead(It.Is>(collection => !collection.Except(joins).Any()), ResourcePipeline.Get, true), - Times.Once()); - - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - - [Fact] - public void AfterRead_Without_Any_Descendant_Hooks_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, _, _) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.AfterRead(articles, ResourcePipeline.Get); - - // Assert - articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - - [Fact] - public void AfterRead_Without_Any_Hook_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); - IHooksDiscovery joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> joinResourceMock, Mock> tagResourceMock) = - CreateTestObjectsC(articleDiscovery, joinDiscovery, tagDiscovery); - - (List
articles, _, _) = CreateIdentifiableManyToManyData(); - - // Act - hookExecutor.AfterRead(articles, ResourcePipeline.Get); - - // Assert - VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/Read/ManyToManyAfterReadTests.cs b/test/UnitTests/ResourceHooks/Executor/Read/ManyToManyAfterReadTests.cs deleted file mode 100644 index d43f862c36..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/Read/ManyToManyAfterReadTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor.Read -{ - public sealed class ManyToManyAfterReadTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.AfterRead - }; - - [Fact] - public void AfterRead() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - - (List
articles, List tags) = CreateManyToManyData(); - - // Act - hookExecutor.AfterRead(articles, ResourcePipeline.Get); - - // Assert - articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); - - tagResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get, true), - Times.Once()); - - VerifyNoOtherCalls(articleResourceMock, tagResourceMock); - } - - [Fact] - public void AfterRead_Without_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - - (List
articles, List tags) = CreateManyToManyData(); - - // Act - hookExecutor.AfterRead(articles, ResourcePipeline.Get); - - // Assert - tagResourceMock.Verify(rd => rd.AfterRead(It.Is>(collection => !collection.Except(tags).Any()), ResourcePipeline.Get, true), - Times.Once()); - - VerifyNoOtherCalls(articleResourceMock, tagResourceMock); - } - - [Fact] - public void AfterRead_Without_Children_Hooks_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(_targetHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - - (List
articles, _) = CreateManyToManyData(); - - // Act - hookExecutor.AfterRead(articles, ResourcePipeline.Get); - - // Assert - articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); - VerifyNoOtherCalls(articleResourceMock, tagResourceMock); - } - - [Fact] - public void AfterRead_Without_Any_Hook_Implemented() - { - // Arrange - IHooksDiscovery
articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); - IHooksDiscovery tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> articleResourceMock, - Mock> tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - - (List
articles, _) = CreateManyToManyData(); - - // Act - hookExecutor.AfterRead(articles, ResourcePipeline.Get); - - // Assert - VerifyNoOtherCalls(articleResourceMock, tagResourceMock); - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/SameResourceTypeTests.cs b/test/UnitTests/ResourceHooks/Executor/SameResourceTypeTests.cs deleted file mode 100644 index 22f49d141a..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/SameResourceTypeTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor -{ - public sealed class SameResourceTypeTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.OnReturn - }; - - [Fact] - public void Resource_Has_Multiple_Relations_To_Same_Type() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - var person1 = new Person(); - - var todo = new TodoItem - { - Owner = person1 - }; - - var person2 = new Person - { - AssignedTodoItems = new HashSet - { - todo - } - }; - - todo.Assignee = person2; - - var person3 = new Person - { - StakeholderTodoItem = todo - }; - - todo.Stakeholders = new HashSet - { - person3 - }; - - List todoList = todo.AsList(); - - // Act - hookExecutor.OnReturn(todoList, ResourcePipeline.Post); - - // Assert - todoResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Post), Times.Once()); - ownerResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Post), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void Resource_Has_Cyclic_Relations() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - (IResourceHookExecutor hookExecutor, Mock> todoResourceMock) = CreateTestObjects(todoDiscovery); - var todo = new TodoItem(); - todo.ParentTodo = todo; - todo.ChildTodoItems = todo.AsList(); - List todoList = todo.AsList(); - - // Act - hookExecutor.OnReturn(todoList, ResourcePipeline.Post); - - // Assert - todoResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Post), Times.Once()); - VerifyNoOtherCalls(todoResourceMock); - } - - [Fact] - public void Resource_Has_Nested_Cyclic_Relations() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - (IResourceHookExecutor hookExecutor, Mock> todoResourceMock) = CreateTestObjects(todoDiscovery); - - var rootTodo = new TodoItem - { - Id = 1 - }; - - var child = new TodoItem - { - ParentTodo = rootTodo, - Id = 2 - }; - - rootTodo.ChildTodoItems = child.AsList(); - - var grandChild = new TodoItem - { - ParentTodo = child, - Id = 3 - }; - - child.ChildTodoItems = grandChild.AsList(); - - var greatGrandChild = new TodoItem - { - ParentTodo = grandChild, - Id = 4 - }; - - grandChild.ChildTodoItems = greatGrandChild.AsList(); - greatGrandChild.ChildTodoItems = rootTodo.AsList(); - List todoList = rootTodo.AsList(); - - // Act - hookExecutor.OnReturn(todoList, ResourcePipeline.Post); - - // Assert - todoResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Post), Times.Exactly(4)); - VerifyNoOtherCalls(todoResourceMock); - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/Update/AfterUpdateTests.cs b/test/UnitTests/ResourceHooks/Executor/Update/AfterUpdateTests.cs deleted file mode 100644 index 5551d364ca..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/Update/AfterUpdateTests.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor.Update -{ - public sealed class AfterUpdateTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.AfterUpdate, - ResourceHook.AfterUpdateRelationship - }; - - [Fact] - public void AfterUpdate() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.AfterUpdate(todoList, ResourcePipeline.Patch); - - // Assert - todoResourceMock.Verify(rd => rd.AfterUpdate(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); - ownerResourceMock.Verify(rd => rd.AfterUpdateRelationship(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void AfterUpdate_Without_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.AfterUpdate(todoList, ResourcePipeline.Patch); - - // Assert - ownerResourceMock.Verify(rd => rd.AfterUpdateRelationship(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void AfterUpdate_Without_Child_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.AfterUpdate(todoList, ResourcePipeline.Patch); - - // Assert - todoResourceMock.Verify(rd => rd.AfterUpdate(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void AfterUpdate_Without_Any_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.AfterUpdate(todoList, ResourcePipeline.Patch); - - // Assert - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/Update/BeforeUpdateTests.cs b/test/UnitTests/ResourceHooks/Executor/Update/BeforeUpdateTests.cs deleted file mode 100644 index 6afd2ce32f..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/Update/BeforeUpdateTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor.Update -{ - public sealed class BeforeUpdateTests : HooksTestsSetup - { - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.BeforeUpdate, - ResourceHook.BeforeUpdateRelationship - }; - - [Fact] - public void BeforeUpdate() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - - // Assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); - - ownerResourceMock.Verify( - rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), ResourcePipeline.Patch), - Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeUpdate_Without_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - - // Assert - ownerResourceMock.Verify( - rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), ResourcePipeline.Patch), - Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeUpdate_Without_Child_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - - // Assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeUpdate_Without_Any_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); - - IEnumerable todoList = CreateTodoWithOwner(); - - // Act - hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - - // Assert - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - } -} diff --git a/test/UnitTests/ResourceHooks/Executor/Update/BeforeUpdateWithDbValuesTests.cs b/test/UnitTests/ResourceHooks/Executor/Update/BeforeUpdateWithDbValuesTests.cs deleted file mode 100644 index 528d61a0e1..0000000000 --- a/test/UnitTests/ResourceHooks/Executor/Update/BeforeUpdateWithDbValuesTests.cs +++ /dev/null @@ -1,291 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using Microsoft.EntityFrameworkCore; -using Moq; -using UnitTests.ResourceHooks.Models; -using Xunit; - -namespace UnitTests.ResourceHooks.Executor.Update -{ - public sealed class BeforeUpdateWithDbValuesTests : HooksTestsSetup - { - private const string Description = "DESCRIPTION"; - private const string Name = "NAME"; - - private readonly ResourceHook[] _targetHooks = - { - ResourceHook.BeforeUpdate, - ResourceHook.BeforeImplicitUpdateRelationship, - ResourceHook.BeforeUpdateRelationship - }; - - private readonly ResourceHook[] _targetHooksNoImplicit = - { - ResourceHook.BeforeUpdate, - ResourceHook.BeforeUpdateRelationship - }; - - private readonly string _personId; - private readonly IList _todoList; - private readonly DbContextOptions _options; - - public BeforeUpdateWithDbValuesTests() - { - _todoList = CreateTodoWithToOnePerson(); - - int todoId = _todoList[0].Id; - int personId = _todoList[0].OneToOnePerson.Id; - _personId = personId.ToString(); - int implicitPersonId = personId + 10000; - - TodoItem implicitTodo = TodoFaker.Generate(); - implicitTodo.Id += 1000; - - implicitTodo.OneToOnePerson = new Person - { - Id = personId, - Name = Name - }; - - implicitTodo.Description = Description + Description; - - _options = InitInMemoryDb(context => - { - context.Set().Add(new TodoItem - { - Id = todoId, - OneToOnePerson = new Person - { - Id = implicitPersonId, - Name = Name + Name - }, - Description = Description - }); - - context.Set().Add(implicitTodo); - context.SaveChanges(); - }); - } - - [Fact] - public void BeforeUpdate() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - // Act - hookExecutor.BeforeUpdate(_todoList, ResourcePipeline.Patch); - - // Assert - todoResourceMock.Verify( - rd => rd.BeforeUpdate(It.Is>(diff => TodoCheckDiff(diff, Description)), ResourcePipeline.Patch), - Times.Once()); - - ownerResourceMock.Verify( - rd => rd.BeforeUpdateRelationship(It.Is>(ids => PersonIdCheck(ids, _personId)), - It.Is>(rh => PersonCheck(Name, rh)), ResourcePipeline.Patch), Times.Once()); - - ownerResourceMock.Verify( - rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => PersonCheck(Name + Name, rh)), ResourcePipeline.Patch), - Times.Once()); - - todoResourceMock.Verify( - rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => TodoCheck(rh, Description + Description)), - ResourcePipeline.Patch), Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeUpdate_Deleting_Relationship() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - - (_, Mock ufMock, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - ufMock.Setup(targetedFields => targetedFields.Relationships) - .Returns(ResourceGraph.GetRelationships((TodoItem todoItem) => todoItem.OneToOnePerson).ToHashSet); - - // Act - var todoList = new List - { - new TodoItem - { - Id = _todoList[0].Id - } - }; - - hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - - // Assert - todoResourceMock.Verify( - rd => rd.BeforeUpdate(It.Is>(diff => TodoCheckDiff(diff, Description)), ResourcePipeline.Patch), - Times.Once()); - - ownerResourceMock.Verify( - rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => PersonCheck(Name + Name, rh)), ResourcePipeline.Patch), - Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeUpdate_Without_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - // Act - hookExecutor.BeforeUpdate(_todoList, ResourcePipeline.Patch); - - // Assert - ownerResourceMock.Verify( - rd => rd.BeforeUpdateRelationship(It.Is>(ids => PersonIdCheck(ids, _personId)), - It.Is>(rh => PersonCheck(Name, rh)), ResourcePipeline.Patch), Times.Once()); - - ownerResourceMock.Verify( - rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => PersonCheck(Name + Name, rh)), ResourcePipeline.Patch), - Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeUpdate_Without_Child_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooks, EnableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - // Act - hookExecutor.BeforeUpdate(_todoList, ResourcePipeline.Patch); - - // Assert - todoResourceMock.Verify( - rd => rd.BeforeUpdate(It.Is>(diff => TodoCheckDiff(diff, Description)), ResourcePipeline.Patch), - Times.Once()); - - todoResourceMock.Verify( - rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => TodoCheck(rh, Description + Description)), - ResourcePipeline.Patch), Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeUpdate_NoImplicit() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooksNoImplicit, ResourceHook.BeforeUpdate); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooksNoImplicit, ResourceHook.BeforeUpdateRelationship); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - // Act - hookExecutor.BeforeUpdate(_todoList, ResourcePipeline.Patch); - - // Assert - todoResourceMock.Verify( - rd => rd.BeforeUpdate(It.Is>(diff => TodoCheckDiff(diff, Description)), ResourcePipeline.Patch), - Times.Once()); - - ownerResourceMock.Verify( - rd => rd.BeforeUpdateRelationship(It.Is>(ids => PersonIdCheck(ids, _personId)), It.IsAny>(), - ResourcePipeline.Patch), Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeUpdate_NoImplicit_Without_Parent_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(_targetHooksNoImplicit, ResourceHook.BeforeUpdateRelationship); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - // Act - hookExecutor.BeforeUpdate(_todoList, ResourcePipeline.Patch); - - // Assert - ownerResourceMock.Verify( - rd => rd.BeforeUpdateRelationship(It.Is>(ids => PersonIdCheck(ids, _personId)), - It.Is>(rh => PersonCheck(Name, rh)), ResourcePipeline.Patch), Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - [Fact] - public void BeforeUpdate_NoImplicit_Without_Child_Hook_Implemented() - { - // Arrange - IHooksDiscovery todoDiscovery = SetDiscoverableHooks(_targetHooksNoImplicit, ResourceHook.BeforeUpdate); - IHooksDiscovery personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - - (_, _, IResourceHookExecutor hookExecutor, Mock> todoResourceMock, - Mock> ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, _options); - - // Act - hookExecutor.BeforeUpdate(_todoList, ResourcePipeline.Patch); - - // Assert - todoResourceMock.Verify( - rd => rd.BeforeUpdate(It.Is>(diff => TodoCheckDiff(diff, Description)), ResourcePipeline.Patch), - Times.Once()); - - VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); - } - - private bool TodoCheckDiff(IDiffableResourceHashSet resources, string checksum) - { - ResourceDiffPair diffPair = resources.GetDiffs().Single(); - bool dbCheck = diffPair.DatabaseValue.Description == checksum; - bool reqCheck = diffPair.Resource.Description == null; - - KeyValuePair> updatedRelationship = resources.GetByRelationship().Single(); - bool diffCheck = updatedRelationship.Key.PublicName == "oneToOnePerson"; - - bool getAffectedCheck = resources.GetAffected(todoItem => todoItem.OneToOnePerson).Any(); - - return dbCheck && reqCheck && diffCheck && getAffectedCheck; - } - - private bool TodoCheck(IRelationshipsDictionary rh, string checksum) - { - return rh.GetByRelationship().Single().Value.First().Description == checksum; - } - - private bool PersonIdCheck(IEnumerable ids, string checksum) - { - return ids.Single() == checksum; - } - - private bool PersonCheck(string checksum, IRelationshipsDictionary helper) - { - IDictionary> entries = helper.GetByRelationship(); - return entries.Single().Value.Single().Name == checksum; - } - } -} diff --git a/test/UnitTests/ResourceHooks/HooksDbContext.cs b/test/UnitTests/ResourceHooks/HooksDbContext.cs deleted file mode 100644 index 2d06f15b33..0000000000 --- a/test/UnitTests/ResourceHooks/HooksDbContext.cs +++ /dev/null @@ -1,66 +0,0 @@ -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; -using UnitTests.ResourceHooks.Models; - -// @formatter:wrap_chained_method_calls chop_always - -namespace UnitTests.ResourceHooks -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class HooksDbContext : DbContext - { - public DbSet TodoItems { get; set; } - public DbSet People { get; set; } - public DbSet
Articles { get; set; } - - public HooksDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder builder) - { - builder.Entity() - .HasOne(todoItem => todoItem.Assignee) - .WithMany(person => person.AssignedTodoItems); - - builder.Entity() - .HasOne(todoItem => todoItem.Owner) - .WithMany(person => person.TodoItems); - - builder.Entity() - .HasKey(bc => new - { - bc.ArticleId, - bc.TagId - }); - - builder.Entity() - .HasKey(bc => new - { - bc.ArticleId, - bc.TagId - }); - - builder.Entity() - .HasOne(person => person.StakeholderTodoItem) - .WithMany(todoItem => todoItem.Stakeholders) - .OnDelete(DeleteBehavior.Cascade); - - builder.Entity() - .HasMany(todoItem => todoItem.ChildTodoItems) - .WithOne(todoItem => todoItem.ParentTodo); - - builder.Entity() - .HasOne(passport => passport.Person) - .WithOne(person => person.Passport) - .HasForeignKey("PassportId") - .OnDelete(DeleteBehavior.SetNull); - - builder.Entity() - .HasOne(todoItem => todoItem.OneToOnePerson) - .WithOne(person => person.OneToOneTodoItem) - .HasForeignKey("OneToOnePersonId"); - } - } -} diff --git a/test/UnitTests/ResourceHooks/HooksDummyData.cs b/test/UnitTests/ResourceHooks/HooksDummyData.cs deleted file mode 100644 index 161a07f48f..0000000000 --- a/test/UnitTests/ResourceHooks/HooksDummyData.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Bogus; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using Microsoft.Extensions.Logging.Abstractions; -using UnitTests.ResourceHooks.Models; -using Person = UnitTests.ResourceHooks.Models.Person; - -namespace UnitTests.ResourceHooks -{ - public class HooksDummyData - { - private readonly Faker _identifiableArticleTagFaker; - protected readonly Faker TodoFaker; - protected readonly Faker PersonFaker; - protected readonly Faker
ArticleFaker; - protected readonly Faker TagFaker; - protected readonly Faker ArticleTagFaker; - protected readonly Faker PassportFaker; - protected IResourceGraph ResourceGraph { get; } - protected ResourceHook[] NoHooks { get; } = new ResourceHook[0]; - - protected ResourceHook[] EnableDbValues { get; } = - { - ResourceHook.BeforeUpdate, - ResourceHook.BeforeUpdateRelationship - }; - - protected ResourceHook[] DisableDbValues { get; } = new ResourceHook[0]; - - protected HooksDummyData() - { - ResourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Add().Add() - .Add
().Add().Add().Build(); - - // @formatter:wrap_chained_method_calls chop_always - // @formatter:keep_existing_linebreaks true - - TodoFaker = new Faker() - .RuleFor(todoItem => todoItem.Id, faker => faker.UniqueIndex + 1); - - PersonFaker = new Faker() - .RuleFor(person => person.Id, faker => faker.UniqueIndex + 1); - - ArticleFaker = new Faker
() - .RuleFor(article => article.Id, faker => faker.UniqueIndex + 1); - - TagFaker = new Faker() - .RuleFor(tag => tag.Id, faker => faker.UniqueIndex + 1); - - ArticleTagFaker = new Faker(); - - _identifiableArticleTagFaker = new Faker() - .RuleFor(identifiableArticleTag => identifiableArticleTag.Id, faker => faker.UniqueIndex + 1); - - PassportFaker = new Faker() - .RuleFor(passport => passport.Id, faker => faker.UniqueIndex + 1); - - // @formatter:wrap_chained_method_calls restore - // @formatter:keep_existing_linebreaks restore - } - - protected IList CreateTodoWithToOnePerson() - { - TodoItem todoItem = TodoFaker.Generate(); - Person person = PersonFaker.Generate(); - List todoList = todoItem.AsList(); - person.OneToOneTodoItem = todoItem; - todoItem.OneToOnePerson = person; - return todoList; - } - - protected IEnumerable CreateTodoWithOwner() - { - TodoItem todoItem = TodoFaker.Generate(); - Person person = PersonFaker.Generate(); - - var todoSet = new HashSet - { - todoItem - }; - - person.AssignedTodoItems = todoSet; - todoItem.Owner = person; - return todoSet; - } - - protected (List
, List) CreateManyToManyData() - { - List tagsSubset = TagFaker.Generate(3); - List joinsSubSet = ArticleTagFaker.Generate(3); - Article articleTagsSubset = ArticleFaker.Generate(); - articleTagsSubset.ArticleTags = joinsSubSet.ToHashSet(); - - for (int index = 0; index < 3; index++) - { - joinsSubSet[index].Article = articleTagsSubset; - joinsSubSet[index].Tag = tagsSubset[index]; - } - - List allTags = TagFaker.Generate(3).Concat(tagsSubset).ToList(); - List completeJoin = ArticleTagFaker.Generate(6); - - Article articleWithAllTags = ArticleFaker.Generate(); - articleWithAllTags.ArticleTags = completeJoin.ToHashSet(); - - for (int index = 0; index < 6; index++) - { - completeJoin[index].Article = articleWithAllTags; - completeJoin[index].Tag = allTags[index]; - } - - List
articles = ArrayFactory.Create(articleTagsSubset, articleWithAllTags).ToList(); - return (articles, allTags); - } - - protected ManyToManyTestData CreateIdentifiableManyToManyData() - { - List tagsSubset = TagFaker.Generate(3); - List joinsSubSet = _identifiableArticleTagFaker.Generate(3); - Article articleTagsSubset = ArticleFaker.Generate(); - articleTagsSubset.IdentifiableArticleTags = joinsSubSet.ToHashSet(); - - for (int index = 0; index < 3; index++) - { - joinsSubSet[index].Article = articleTagsSubset; - joinsSubSet[index].Tag = tagsSubset[index]; - } - - List allTags = TagFaker.Generate(3).Concat(tagsSubset).ToList(); - List completeJoin = _identifiableArticleTagFaker.Generate(6); - - Article articleWithAllTags = ArticleFaker.Generate(); - articleWithAllTags.IdentifiableArticleTags = joinsSubSet.ToHashSet(); - - for (int index = 0; index < 6; index++) - { - completeJoin[index].Article = articleWithAllTags; - completeJoin[index].Tag = allTags[index]; - } - - List allJoins = joinsSubSet.Concat(completeJoin).ToList(); - List
articles = ArrayFactory.Create(articleTagsSubset, articleWithAllTags).ToList(); - return new ManyToManyTestData(articles, allJoins, allTags); - } - - protected sealed class ManyToManyTestData - { - public List
Articles { get; } - public List ArticleTags { get; } - public List Tags { get; } - - public ManyToManyTestData(List
articles, List articleTags, List tags) - { - Articles = articles; - ArticleTags = articleTags; - Tags = tags; - } - - public void Deconstruct(out List
articles, out List articleTags, out List tags) - { - articles = Articles; - articleTags = ArticleTags; - tags = Tags; - } - } - } -} diff --git a/test/UnitTests/ResourceHooks/HooksTestsSetup.cs b/test/UnitTests/ResourceHooks/HooksTestsSetup.cs deleted file mode 100644 index 27b22d3cda..0000000000 --- a/test/UnitTests/ResourceHooks/HooksTestsSetup.cs +++ /dev/null @@ -1,408 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Discovery; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Hooks.Internal.Traversal; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Repositories; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using UnitTests.ResourceHooks.Models; - -namespace UnitTests.ResourceHooks -{ - public class HooksTestsSetup : HooksDummyData - { - private static readonly IncludeChainConverter IncludeChainConverter = new IncludeChainConverter(); - private static readonly HooksObjectFactory ObjectFactory = new HooksObjectFactory(); - - private TestMocks CreateMocks() - { - var genericServiceFactoryMock = new Mock(); - var targetedFieldsMock = new Mock(); - - var constraintsMock = new Mock>(); - constraintsMock.Setup(providers => providers.GetEnumerator()).Returns(Enumerable.Empty().GetEnumerator()); - - var optionsMock = new JsonApiOptions - { - LoadDatabaseValues = false - }; - - return new TestMocks(targetedFieldsMock, constraintsMock, genericServiceFactoryMock, optionsMock); - } - - protected TestObjectsA CreateTestObjects(IHooksDiscovery primaryDiscovery = null) - where TPrimary : class, IIdentifiable - { - // creates the resource definition mock and corresponding ImplementedHooks discovery instance - Mock> primaryResource = CreateResourceDefinition(); - - // mocking the genericServiceFactory and JsonApiContext and wiring them up. - (Mock ufMock, Mock> constraintsMock, Mock gpfMock, - IJsonApiOptions options) = CreateMocks(); - - SetupProcessorFactoryForResourceDefinition(gpfMock, primaryResource.Object, primaryDiscovery); - - var execHelper = new HookContainerProvider(gpfMock.Object, ResourceGraph, options); - var traversalHelper = new NodeNavigator(ResourceGraph, ufMock.Object); - var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, ResourceGraph); - - return new TestObjectsA(hookExecutor, primaryResource); - } - - protected TestObjectsB CreateTestObjects(IHooksDiscovery primaryDiscovery = null, - IHooksDiscovery secondaryDiscovery = null, DbContextOptions repoDbContextOptions = null) - where TPrimary : class, IIdentifiable - where TSecondary : class, IIdentifiable - { - // creates the resource definition mock and corresponding for a given set of discoverable hooks - Mock> primaryResource = CreateResourceDefinition(); - Mock> secondaryResource = CreateResourceDefinition(); - - // mocking the genericServiceFactory and JsonApiContext and wiring them up. - (Mock ufMock, Mock> constraintsMock, Mock gpfMock, - IJsonApiOptions options) = CreateMocks(); - - HooksDbContext dbContext = repoDbContextOptions != null ? new HooksDbContext(repoDbContextOptions) : null; - - IResourceGraph resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Add().Build(); - - SetupProcessorFactoryForResourceDefinition(gpfMock, primaryResource.Object, primaryDiscovery, dbContext, resourceGraph); - SetupProcessorFactoryForResourceDefinition(gpfMock, secondaryResource.Object, secondaryDiscovery, dbContext, resourceGraph); - - var execHelper = new HookContainerProvider(gpfMock.Object, ResourceGraph, options); - var traversalHelper = new NodeNavigator(ResourceGraph, ufMock.Object); - var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, ResourceGraph); - - return new TestObjectsB(constraintsMock, ufMock, hookExecutor, primaryResource, secondaryResource); - } - - protected TestObjectsC CreateTestObjectsC( - IHooksDiscovery primaryDiscovery = null, IHooksDiscovery firstSecondaryDiscovery = null, - IHooksDiscovery secondSecondaryDiscovery = null, DbContextOptions repoDbContextOptions = null) - where TPrimary : class, IIdentifiable - where TFirstSecondary : class, IIdentifiable - where TSecondSecondary : class, IIdentifiable - { - // creates the resource definition mock and corresponding for a given set of discoverable hooks - Mock> primaryResource = CreateResourceDefinition(); - Mock> firstSecondaryResource = CreateResourceDefinition(); - Mock> secondSecondaryResource = CreateResourceDefinition(); - - // mocking the genericServiceFactory and JsonApiContext and wiring them up. - (Mock ufMock, Mock> constraintsMock, Mock gpfMock, - IJsonApiOptions options) = CreateMocks(); - - HooksDbContext dbContext = repoDbContextOptions != null ? new HooksDbContext(repoDbContextOptions) : null; - - IResourceGraph resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Add() - .Add().Build(); - - SetupProcessorFactoryForResourceDefinition(gpfMock, primaryResource.Object, primaryDiscovery, dbContext, resourceGraph); - SetupProcessorFactoryForResourceDefinition(gpfMock, firstSecondaryResource.Object, firstSecondaryDiscovery, dbContext, resourceGraph); - SetupProcessorFactoryForResourceDefinition(gpfMock, secondSecondaryResource.Object, secondSecondaryDiscovery, dbContext, resourceGraph); - - var execHelper = new HookContainerProvider(gpfMock.Object, ResourceGraph, options); - var traversalHelper = new NodeNavigator(ResourceGraph, ufMock.Object); - var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, ResourceGraph); - - return new TestObjectsC(constraintsMock, hookExecutor, primaryResource, firstSecondaryResource, - secondSecondaryResource); - } - - protected IHooksDiscovery SetDiscoverableHooks(ResourceHook[] implementedHooks, params ResourceHook[] enableDbValuesHooks) - where TResource : class, IIdentifiable - { - var mock = new Mock>(); - mock.Setup(discovery => discovery.ImplementedHooks).Returns(implementedHooks); - - if (!enableDbValuesHooks.Any()) - { - mock.Setup(discovery => discovery.DatabaseValuesDisabledHooks).Returns(enableDbValuesHooks); - } - - mock.Setup(discovery => discovery.DatabaseValuesEnabledHooks) - .Returns(ResourceHook.BeforeImplicitUpdateRelationship.AsEnumerable().Concat(enableDbValuesHooks).ToArray()); - - return mock.Object; - } - - protected void VerifyNoOtherCalls(params dynamic[] resourceMocks) - { - foreach (dynamic mock in resourceMocks) - { - mock.VerifyNoOtherCalls(); - } - } - - protected DbContextOptions InitInMemoryDb(Action seeder) - { - DbContextOptions options = new DbContextOptionsBuilder().UseInMemoryDatabase("repository_mock").Options; - - using var context = new HooksDbContext(options); - - seeder(context); - ResolveInverseRelationships(context); - - return options; - } - - private void MockHooks(Mock> resourceDefinition) - where TModel : class, IIdentifiable - { - resourceDefinition.Setup(rd => rd.BeforeCreate(It.IsAny>(), It.IsAny())) - .Returns, ResourcePipeline>((resources, _) => resources).Verifiable(); - - resourceDefinition.Setup(rd => rd.BeforeRead(It.IsAny(), It.IsAny(), It.IsAny())).Verifiable(); - - resourceDefinition.Setup(rd => rd.BeforeUpdate(It.IsAny>(), It.IsAny())) - .Returns, ResourcePipeline>((resources, _) => resources).Verifiable(); - - resourceDefinition.Setup(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny())) - .Returns, ResourcePipeline>((resources, _) => resources).Verifiable(); - - resourceDefinition - .Setup(rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), - It.IsAny())).Returns, IRelationshipsDictionary, ResourcePipeline>((ids, _, __) => ids) - .Verifiable(); - - resourceDefinition.Setup(rd => rd.BeforeImplicitUpdateRelationship(It.IsAny>(), It.IsAny())) - .Verifiable(); - - resourceDefinition.Setup(rd => rd.OnReturn(It.IsAny>(), It.IsAny())) - .Returns, ResourcePipeline>((resources, _) => resources).Verifiable(); - - resourceDefinition.Setup(rd => rd.AfterCreate(It.IsAny>(), It.IsAny())).Verifiable(); - resourceDefinition.Setup(rd => rd.AfterRead(It.IsAny>(), It.IsAny(), It.IsAny())).Verifiable(); - resourceDefinition.Setup(rd => rd.AfterUpdate(It.IsAny>(), It.IsAny())).Verifiable(); - resourceDefinition.Setup(rd => rd.AfterDelete(It.IsAny>(), It.IsAny(), It.IsAny())).Verifiable(); - } - - private void SetupProcessorFactoryForResourceDefinition(Mock processorFactory, - IResourceHookContainer modelResource, IHooksDiscovery discovery, HooksDbContext dbContext = null, - IResourceGraph resourceGraph = null) - where TModel : class, IIdentifiable - { - processorFactory.Setup(factory => factory.Get(typeof(ResourceHooksDefinition<>), typeof(TModel))).Returns(modelResource); - - processorFactory.Setup(factory => factory.Get(typeof(IHooksDiscovery<>), typeof(TModel))).Returns(discovery); - - if (dbContext != null) - { - Type idType = ObjectFactory.GetIdType(typeof(TModel)); - - if (idType == typeof(int)) - { - IResourceReadRepository repo = CreateTestRepository(dbContext, resourceGraph); - - processorFactory.Setup(factory => - factory.Get>(typeof(IResourceReadRepository<,>), typeof(TModel), typeof(int))).Returns(repo); - } - else - { - throw new TypeLoadException("Test not set up properly"); - } - } - } - - private IResourceReadRepository CreateTestRepository(HooksDbContext dbContext, IResourceGraph resourceGraph) - where TModel : class, IIdentifiable - { - var resourceFactory = new TestResourceFactory(); - IDbContextResolver resolver = CreateTestDbResolver(dbContext); - var targetedFields = new TargetedFields(); - - return new EntityFrameworkCoreRepository(targetedFields, resolver, resourceGraph, resourceFactory, - Enumerable.Empty(), NullLoggerFactory.Instance); - } - - private IDbContextResolver CreateTestDbResolver(HooksDbContext dbContext) - { - var mock = new Mock(); - mock.Setup(resolver => resolver.GetContext()).Returns(dbContext); - return mock.Object; - } - - private void ResolveInverseRelationships(HooksDbContext context) - { - IEnumerable> dbContextResolvers = new DbContextResolver(context).AsEnumerable(); - var inverseRelationships = new InverseNavigationResolver(ResourceGraph, dbContextResolvers); - inverseRelationships.Resolve(); - } - - private Mock> CreateResourceDefinition() - where TModel : class, IIdentifiable - { - var resourceDefinition = new Mock>(); - MockHooks(resourceDefinition); - return resourceDefinition; - } - - protected IncludeExpression ToIncludeExpression(params string[] includePaths) - { - var relationshipChains = new List(); - - foreach (string includePath in includePaths) - { - ResourceFieldChainExpression relationshipChain = GetRelationshipsInPath(includePath); - relationshipChains.Add(relationshipChain); - } - - return IncludeChainConverter.FromRelationshipChains(relationshipChains); - } - - private ResourceFieldChainExpression GetRelationshipsInPath(string includePath) - { - ResourceContext resourceContext = ResourceGraph.GetResourceContext(); - var relationships = new List(); - - foreach (string relationshipName in includePath.Split('.')) - { - RelationshipAttribute relationship = resourceContext.Relationships.Single(nextRelationship => nextRelationship.PublicName == relationshipName); - - relationships.Add(relationship); - - resourceContext = ResourceGraph.GetResourceContext(relationship.RightType); - } - - return new ResourceFieldChainExpression(relationships); - } - - protected IEnumerable Wrap(IncludeExpression includeExpression) - { - var expressionsInScope = new List - { - new ExpressionInScope(null, includeExpression) - }; - - var mock = new Mock(); - mock.Setup(provider => provider.GetConstraints()).Returns(expressionsInScope); - - IQueryConstraintProvider includeConstraintProvider = mock.Object; - return includeConstraintProvider.AsEnumerable(); - } - - private sealed class TestMocks - { - public Mock TargetedFieldsMock { get; } - public Mock> ConstraintsMock { get; } - public Mock GenericServiceFactoryMock { get; } - public IJsonApiOptions Options { get; } - - public TestMocks(Mock targetedFieldsMock, Mock> constraintsMock, - Mock genericServiceFactoryMock, IJsonApiOptions options) - { - TargetedFieldsMock = targetedFieldsMock; - ConstraintsMock = constraintsMock; - GenericServiceFactoryMock = genericServiceFactoryMock; - Options = options; - } - - public void Deconstruct(out Mock targetedFieldsMock, out Mock> constraintsMock, - out Mock genericServiceFactoryMock, out IJsonApiOptions optionsMock) - { - targetedFieldsMock = TargetedFieldsMock; - constraintsMock = ConstraintsMock; - genericServiceFactoryMock = GenericServiceFactoryMock; - optionsMock = Options; - } - } - - protected sealed class TestObjectsA - where TPrimary : class, IIdentifiable - { - public IResourceHookExecutor HookExecutor { get; } - public Mock> PrimaryResourceContainerMock { get; } - - public TestObjectsA(IResourceHookExecutor hookExecutor, Mock> primaryResourceContainerMock) - { - HookExecutor = hookExecutor; - PrimaryResourceContainerMock = primaryResourceContainerMock; - } - - public void Deconstruct(out IResourceHookExecutor hookExecutor, out Mock> primaryResourceContainerMock) - { - hookExecutor = HookExecutor; - primaryResourceContainerMock = PrimaryResourceContainerMock; - } - } - - protected sealed class TestObjectsB - where TPrimary : class, IIdentifiable - where TSecondary : class, IIdentifiable - { - public Mock> ConstraintsMock { get; } - public Mock TargetedFieldsMock { get; } - public IResourceHookExecutor HookExecutor { get; } - public Mock> PrimaryResourceContainerMock { get; } - public Mock> SecondaryResourceContainerMock { get; } - - public TestObjectsB(Mock> constraintsMock, Mock targetedFieldsMock, - IResourceHookExecutor hookExecutor, Mock> primaryResourceContainerMock, - Mock> secondaryResourceContainerMock) - { - ConstraintsMock = constraintsMock; - TargetedFieldsMock = targetedFieldsMock; - HookExecutor = hookExecutor; - PrimaryResourceContainerMock = primaryResourceContainerMock; - SecondaryResourceContainerMock = secondaryResourceContainerMock; - } - - public void Deconstruct(out Mock> constraintsMock, out Mock ufMock, - out IResourceHookExecutor hookExecutor, out Mock> primaryResource, - out Mock> secondaryResource) - { - constraintsMock = ConstraintsMock; - ufMock = TargetedFieldsMock; - hookExecutor = HookExecutor; - primaryResource = PrimaryResourceContainerMock; - secondaryResource = SecondaryResourceContainerMock; - } - } - - protected sealed class TestObjectsC - where TPrimary : class, IIdentifiable - where TFirstSecondary : class, IIdentifiable - where TSecondSecondary : class, IIdentifiable - { - public Mock> ConstraintsMock { get; } - public IResourceHookExecutor HookExecutor { get; } - public Mock> PrimaryResourceContainerMock { get; } - public Mock> FirstSecondaryResourceContainerMock { get; } - public Mock> SecondSecondaryResourceContainerMock { get; } - - public TestObjectsC(Mock> constraintsMock, IResourceHookExecutor hookExecutor, - Mock> primaryResourceContainerMock, - Mock> firstSecondaryResourceContainerMock, - Mock> secondSecondaryResourceContainerMock) - { - ConstraintsMock = constraintsMock; - HookExecutor = hookExecutor; - PrimaryResourceContainerMock = primaryResourceContainerMock; - FirstSecondaryResourceContainerMock = firstSecondaryResourceContainerMock; - SecondSecondaryResourceContainerMock = secondSecondaryResourceContainerMock; - } - - public void Deconstruct(out Mock> constraintsMock, out IResourceHookExecutor hookExecutor, - out Mock> primaryResourceContainerMock, - out Mock> firstSecondaryResourceContainerMock, - out Mock> secondSecondaryResourceContainerMock) - { - constraintsMock = ConstraintsMock; - hookExecutor = HookExecutor; - primaryResourceContainerMock = PrimaryResourceContainerMock; - firstSecondaryResourceContainerMock = FirstSecondaryResourceContainerMock; - secondSecondaryResourceContainerMock = SecondSecondaryResourceContainerMock; - } - } - } -} diff --git a/test/UnitTests/ResourceHooks/Models/Article.cs b/test/UnitTests/ResourceHooks/Models/Article.cs deleted file mode 100644 index 037e29075b..0000000000 --- a/test/UnitTests/ResourceHooks/Models/Article.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace UnitTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Article : Identifiable - { - [NotMapped] - [HasManyThrough(nameof(ArticleTags))] - public ISet Tags { get; set; } - - public ISet ArticleTags { get; set; } - - [NotMapped] - [HasManyThrough(nameof(IdentifiableArticleTags))] - public ICollection IdentifiableTags { get; set; } - - public ICollection IdentifiableArticleTags { get; set; } - } -} diff --git a/test/UnitTests/ResourceHooks/Models/ArticleTag.cs b/test/UnitTests/ResourceHooks/Models/ArticleTag.cs deleted file mode 100644 index 2c113589c7..0000000000 --- a/test/UnitTests/ResourceHooks/Models/ArticleTag.cs +++ /dev/null @@ -1,14 +0,0 @@ -using JetBrains.Annotations; - -namespace UnitTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class ArticleTag - { - public int ArticleId { get; set; } - public Article Article { get; set; } - - public int TagId { get; set; } - public Tag Tag { get; set; } - } -} diff --git a/test/UnitTests/ResourceHooks/Models/IdentifiableArticleTag.cs b/test/UnitTests/ResourceHooks/Models/IdentifiableArticleTag.cs deleted file mode 100644 index 452e6767a9..0000000000 --- a/test/UnitTests/ResourceHooks/Models/IdentifiableArticleTag.cs +++ /dev/null @@ -1,20 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace UnitTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class IdentifiableArticleTag : Identifiable - { - public int ArticleId { get; set; } - - [HasOne] - public Article Article { get; set; } - - public int TagId { get; set; } - - [HasOne] - public Tag Tag { get; set; } - } -} diff --git a/test/UnitTests/ResourceHooks/Models/Passport.cs b/test/UnitTests/ResourceHooks/Models/Passport.cs deleted file mode 100644 index ca001feb66..0000000000 --- a/test/UnitTests/ResourceHooks/Models/Passport.cs +++ /dev/null @@ -1,13 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace UnitTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Passport : Identifiable - { - [HasOne] - public Person Person { get; set; } - } -} diff --git a/test/UnitTests/ResourceHooks/Models/Person.cs b/test/UnitTests/ResourceHooks/Models/Person.cs deleted file mode 100644 index 55ee5ea8ae..0000000000 --- a/test/UnitTests/ResourceHooks/Models/Person.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace UnitTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Person : Identifiable - { - [Attr] - public string Name { get; set; } - - [HasMany] - public ISet TodoItems { get; set; } - - [HasMany] - public ISet AssignedTodoItems { get; set; } - - [HasOne] - public TodoItem OneToOneTodoItem { get; set; } - - [HasOne] - public TodoItem StakeholderTodoItem { get; set; } - - [HasOne] - public Passport Passport { get; set; } - } -} diff --git a/test/UnitTests/ResourceHooks/Models/Tag.cs b/test/UnitTests/ResourceHooks/Models/Tag.cs deleted file mode 100644 index f56e084a23..0000000000 --- a/test/UnitTests/ResourceHooks/Models/Tag.cs +++ /dev/null @@ -1,13 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace UnitTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Tag : Identifiable - { - [Attr] - public string Name { get; set; } - } -} diff --git a/test/UnitTests/ResourceHooks/Models/TodoItem.cs b/test/UnitTests/ResourceHooks/Models/TodoItem.cs deleted file mode 100644 index 0b7764bff6..0000000000 --- a/test/UnitTests/ResourceHooks/Models/TodoItem.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace UnitTests.ResourceHooks.Models -{ - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class TodoItem : Identifiable - { - [Attr] - public string Description { get; set; } - - [HasOne] - public Person Owner { get; set; } - - [HasOne] - public Person Assignee { get; set; } - - [HasOne] - public Person OneToOnePerson { get; set; } - - [HasMany] - public ISet Stakeholders { get; set; } - - // cyclical to-many structure - [HasOne] - public TodoItem ParentTodo { get; set; } - - [HasMany] - public IList ChildTodoItems { get; set; } - } -} diff --git a/test/UnitTests/ResourceHooks/NotTargeted.cs b/test/UnitTests/ResourceHooks/NotTargeted.cs deleted file mode 100644 index 82e312e750..0000000000 --- a/test/UnitTests/ResourceHooks/NotTargeted.cs +++ /dev/null @@ -1,10 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; - -namespace UnitTests.ResourceHooks -{ - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - internal sealed class NotTargeted : Identifiable - { - } -} diff --git a/test/UnitTests/ResourceHooks/RelationshipDictionaryTests.cs b/test/UnitTests/ResourceHooks/RelationshipDictionaryTests.cs deleted file mode 100644 index 454d2083af..0000000000 --- a/test/UnitTests/ResourceHooks/RelationshipDictionaryTests.cs +++ /dev/null @@ -1,311 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using JetBrains.Annotations; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources.Annotations; -using Xunit; - -namespace UnitTests.ResourceHooks -{ - public sealed class RelationshipDictionaryTests - { - private readonly HasOneAttribute _firstToOneAttr; - private readonly HasOneAttribute _secondToOneAttr; - private readonly HasManyAttribute _toManyAttr; - - private readonly Dictionary> _relationships = new Dictionary>(); - - private readonly HashSet _firstToOnesResources = new HashSet - { - new Dummy - { - Id = 1 - }, - new Dummy - { - Id = 2 - }, - new Dummy - { - Id = 3 - } - }; - - private readonly HashSet _secondToOnesResources = new HashSet - { - new Dummy - { - Id = 4 - }, - new Dummy - { - Id = 5 - }, - new Dummy - { - Id = 6 - } - }; - - private readonly HashSet _toManiesResources = new HashSet - { - new Dummy - { - Id = 7 - }, - new Dummy - { - Id = 8 - }, - new Dummy - { - Id = 9 - } - }; - - private readonly HashSet _noRelationshipsResources = new HashSet - { - new Dummy - { - Id = 10 - }, - new Dummy - { - Id = 11 - }, - new Dummy - { - Id = 12 - } - }; - - private readonly HashSet _allResources; - - public RelationshipDictionaryTests() - { - _firstToOneAttr = new HasOneAttribute - { - PublicName = "firstToOne", - LeftType = typeof(Dummy), - RightType = typeof(ToOne), - Property = typeof(Dummy).GetProperty(nameof(Dummy.FirstToOne)) - }; - - _secondToOneAttr = new HasOneAttribute - { - PublicName = "secondToOne", - LeftType = typeof(Dummy), - RightType = typeof(ToOne), - Property = typeof(Dummy).GetProperty(nameof(Dummy.SecondToOne)) - }; - - _toManyAttr = new HasManyAttribute - { - PublicName = "toManies", - LeftType = typeof(Dummy), - RightType = typeof(ToMany), - Property = typeof(Dummy).GetProperty(nameof(Dummy.ToManies)) - }; - - _relationships.Add(_firstToOneAttr, _firstToOnesResources); - _relationships.Add(_secondToOneAttr, _secondToOnesResources); - _relationships.Add(_toManyAttr, _toManiesResources); - _allResources = new HashSet(_firstToOnesResources.Union(_secondToOnesResources).Union(_toManiesResources).Union(_noRelationshipsResources)); - } - - [Fact] - public void RelationshipsDictionary_GetByRelationships() - { - // Arrange - var relationshipsDictionary = new RelationshipsDictionary(_relationships); - - // Act - IDictionary> toOnes = relationshipsDictionary.GetByRelationship(); - IDictionary> toManies = relationshipsDictionary.GetByRelationship(); - IDictionary> notTargeted = relationshipsDictionary.GetByRelationship(); - - // Assert - AssertRelationshipDictionaryGetters(relationshipsDictionary, toOnes, toManies, notTargeted); - } - - [Fact] - public void RelationshipsDictionary_GetAffected() - { - // Arrange - var relationshipsDictionary = new RelationshipsDictionary(_relationships); - - // Act - List affectedThroughFirstToOne = relationshipsDictionary.GetAffected(action => action.FirstToOne).ToList(); - List affectedThroughSecondToOne = relationshipsDictionary.GetAffected(action => action.SecondToOne).ToList(); - List affectedThroughToMany = relationshipsDictionary.GetAffected(action => action.ToManies).ToList(); - - // Assert - affectedThroughFirstToOne.ForEach(resource => Assert.Contains(resource, _firstToOnesResources)); - affectedThroughSecondToOne.ForEach(resource => Assert.Contains(resource, _secondToOnesResources)); - affectedThroughToMany.ForEach(resource => Assert.Contains(resource, _toManiesResources)); - } - - [Fact] - public void ResourceHashSet_GetByRelationships() - { - // Arrange - var resources = new ResourceHashSet(_allResources, _relationships); - - // Act - IDictionary> toOnes = resources.GetByRelationship(); - IDictionary> toManies = resources.GetByRelationship(); - IDictionary> notTargeted = resources.GetByRelationship(); - IDictionary> allRelationships = resources.AffectedRelationships; - - // Assert - AssertRelationshipDictionaryGetters(allRelationships, toOnes, toManies, notTargeted); - List allResourcesWithAffectedRelationships = allRelationships.SelectMany(pair => pair.Value).ToList(); - - _noRelationshipsResources.ToList().ForEach(resource => - { - Assert.DoesNotContain(resource, allResourcesWithAffectedRelationships); - }); - } - - [Fact] - public void ResourceDiff_GetByRelationships() - { - // Arrange - var dbResources = new HashSet(_allResources.Select(resource => new Dummy - { - Id = resource.Id - }).ToList()); - - var diffs = new DiffableResourceHashSet(_allResources, dbResources, _relationships, null); - - // Act - IDictionary> toOnes = diffs.GetByRelationship(); - IDictionary> toManies = diffs.GetByRelationship(); - IDictionary> notTargeted = diffs.GetByRelationship(); - IDictionary> allRelationships = diffs.AffectedRelationships; - - // Assert - AssertRelationshipDictionaryGetters(allRelationships, toOnes, toManies, notTargeted); - List allResourcesWithAffectedRelationships = allRelationships.SelectMany(pair => pair.Value).ToList(); - - _noRelationshipsResources.ToList().ForEach(resource => - { - Assert.DoesNotContain(resource, allResourcesWithAffectedRelationships); - }); - - DiffableResourceHashSet requestResourcesFromDiff = diffs; - - requestResourcesFromDiff.ToList().ForEach(resource => - { - Assert.Contains(resource, _allResources); - }); - - IEnumerable databaseResourcesFromDiff = diffs.GetDiffs().Select(pair => pair.DatabaseValue); - - databaseResourcesFromDiff.ToList().ForEach(resource => - { - Assert.Contains(resource, dbResources); - }); - } - - [Fact] - public void ResourceDiff_Loops_Over_Diffs() - { - // Arrange - var dbResources = new HashSet(_allResources.Select(resource => new Dummy - { - Id = resource.Id - })); - - var diffs = new DiffableResourceHashSet(_allResources, dbResources, _relationships, null); - - // Act - ResourceDiffPair[] resourceDiffPairs = diffs.GetDiffs().ToArray(); - - // Assert - foreach (ResourceDiffPair diff in resourceDiffPairs) - { - Assert.Equal(diff.Resource.Id, diff.DatabaseValue.Id); - Assert.NotEqual(diff.Resource, diff.DatabaseValue); - Assert.Contains(diff.Resource, _allResources); - Assert.Contains(diff.DatabaseValue, dbResources); - } - } - - [Fact] - public void ResourceDiff_GetAffected_Relationships() - { - // Arrange - var dbResources = new HashSet(_allResources.Select(resource => new Dummy - { - Id = resource.Id - })); - - var diffs = new DiffableResourceHashSet(_allResources, dbResources, _relationships, null); - - // Act - List affectedThroughFirstToOne = diffs.GetAffected(action => action.FirstToOne).ToList(); - List affectedThroughSecondToOne = diffs.GetAffected(action => action.SecondToOne).ToList(); - List affectedThroughToMany = diffs.GetAffected(action => action.ToManies).ToList(); - - // Assert - affectedThroughFirstToOne.ForEach(resource => Assert.Contains(resource, _firstToOnesResources)); - affectedThroughSecondToOne.ForEach(resource => Assert.Contains(resource, _secondToOnesResources)); - affectedThroughToMany.ForEach(resource => Assert.Contains(resource, _toManiesResources)); - } - - [Fact] - public void ResourceDiff_GetAffected_Attributes() - { - // Arrange - var dbResources = new HashSet(_allResources.Select(resource => new Dummy - { - Id = resource.Id - })); - - var updatedAttributes = new Dictionary> - { - { typeof(Dummy).GetProperty(nameof(Dummy.SomeUpdatedProperty))!, _allResources } - }; - - var diffs = new DiffableResourceHashSet(_allResources, dbResources, _relationships, updatedAttributes); - - // Act - HashSet affectedThroughSomeUpdatedProperty = diffs.GetAffected(action => action.SomeUpdatedProperty); - HashSet affectedThroughSomeNotUpdatedProperty = diffs.GetAffected(action => action.SomeNotUpdatedProperty); - - // Assert - Assert.NotEmpty(affectedThroughSomeUpdatedProperty); - Assert.Empty(affectedThroughSomeNotUpdatedProperty); - } - - [AssertionMethod] - private void AssertRelationshipDictionaryGetters(IDictionary> relationshipsDictionary, - IDictionary> toOnes, IDictionary> toManies, - IDictionary> notTargeted) - { - Assert.Contains(_firstToOneAttr, toOnes.Keys); - Assert.Contains(_secondToOneAttr, toOnes.Keys); - Assert.Contains(_toManyAttr, toManies.Keys); - Assert.Equal(relationshipsDictionary.Keys.Count, toOnes.Keys.Count + toManies.Keys.Count + notTargeted.Keys.Count); - - toOnes[_firstToOneAttr].ToList().ForEach(resource => - { - Assert.Contains(resource, _firstToOnesResources); - }); - - toOnes[_secondToOneAttr].ToList().ForEach(resource => - { - Assert.Contains(resource, _secondToOnesResources); - }); - - toManies[_toManyAttr].ToList().ForEach(resource => - { - Assert.Contains(resource, _toManiesResources); - }); - - Assert.Empty(notTargeted); - } - } -} diff --git a/test/UnitTests/ResourceHooks/ToMany.cs b/test/UnitTests/ResourceHooks/ToMany.cs deleted file mode 100644 index c77489a62f..0000000000 --- a/test/UnitTests/ResourceHooks/ToMany.cs +++ /dev/null @@ -1,8 +0,0 @@ -using JsonApiDotNetCore.Resources; - -namespace UnitTests.ResourceHooks -{ - public sealed class ToMany : Identifiable - { - } -} diff --git a/test/UnitTests/ResourceHooks/ToOne.cs b/test/UnitTests/ResourceHooks/ToOne.cs deleted file mode 100644 index 89b0c60be4..0000000000 --- a/test/UnitTests/ResourceHooks/ToOne.cs +++ /dev/null @@ -1,8 +0,0 @@ -using JsonApiDotNetCore.Resources; - -namespace UnitTests.ResourceHooks -{ - public sealed class ToOne : Identifiable - { - } -} diff --git a/wiki/v4/content/deprecation.md b/wiki/v4/content/deprecation.md deleted file mode 100644 index 65caab195b..0000000000 --- a/wiki/v4/content/deprecation.md +++ /dev/null @@ -1,5 +0,0 @@ -# Deprecation - -* Bulk -* Operations -* Resource entity seperation diff --git a/wiki/v4/content/query-parameter-services.md b/wiki/v4/content/query-parameter-services.md deleted file mode 100644 index 73bdd84f55..0000000000 --- a/wiki/v4/content/query-parameter-services.md +++ /dev/null @@ -1,123 +0,0 @@ -# Query Parameter Services - -This article describes -1. how URL query parameters are currently processed internally -2. how to customize the behaviour of existing query parameters -3. how to register your own - -## 1. Internal usage - -Below is a list of the query parameters that are supported. Each supported query parameter has it's own dedicated service. - -| Query Parameter Service | Occurence in URL | Domain | -|-------------------------|----------------------------------|-------------------------------------------------------| -| `IFilterService` | `?filter[article.title]=title` | filtering the resultset | -| `IIncludeService` | `?include=article.author` | including related data | -| `IPageService` | `?page[size]=10&page[number]=3` | pagination of the resultset | -| `ISortService` | `?sort=-title` | sorting the resultset | -| `ISparseFieldsService` | `?fields[article]=title,summary` | sparse field selection | -| `IDefaultsService` | `?default=false` | omitting default values from the serialization result, eg `guid-value": "00000000-0000-0000-0000-000000000000"` | -| `INullsService` | `?nulls=true` | omitting null values from the serialization result | - - -These services are responsible for parsing the value from the URL by gathering relevant (meta)data and performing validations as required by JsonApiDotNetCore down the pipeline. For example, the `IIncludeService` is responsible for checking if `article.author` is a valid relationship chain, and pre-processes the chain into a `List` so that the rest of the framework can process it easier. - -Each of these services implement the `IQueryParameterService` interface, which exposes: -* a `Name` property that is used internally to match the URL query parameter to the service. - `IIncludeService.Name` returns `include`, which will match `include=article.author` -* a `Parse` method that is called internally in the middleware to process the url query parameters. - - -```c# -public interface IQueryParameterService -{ - /// - /// Parses the value of the query parameter. Invoked in the middleware. - /// - /// the value of the query parameter as retrieved from the url - void Parse(KeyValuePair queryParameter); - /// - /// The name of the query parameter as matched in the URL query string. - /// - string Name { get; } -} -``` - -The piece of internals that is responsible for calling the `Parse` method is the `IQueryParameterDiscovery` service (formally known as `QueryParser`). This service injects every registered implementation of `IQueryParameterService` and calls the parse method with the appropiate part of the url querystring. - - -## 2. Customizing behaviour -You can register your own implementation of every service interface in the table above. As an example, we may want to add additional support for `page[index]=3` next to `page[number]=3` ("number" replaced with "index"). This could be achieved as follows - -```c# -// CustomPageService.cs -public class CustomPageService : PageService -{ - public override void Parse(KeyValuePair queryParameter) - { - var key = queryParameter.Key.Replace("index", "number"); - queryParameter = KeyValuePair(key, queryParameter.Value); - base.Parse(queryParameter) - } -} - -// Startup.cs -services.AddScoped(); -``` - -## 3. Registering new services -You may also define an entirely new custom query parameter. For example, we want to trigger a `HTTP 418 I'm a teapot` if a client includes a `?teapot=true` query parameter. This could be implemented as follows: - - -```c# -// ITeapotService.cs -public interface ITeapotService -{ - // Interface containing the "business logic" of the query parameter service, - // in a way useful to your application - bool ShouldThrowTeapot { get; } -} - -// TeapotService.cs -public class TeapotService : IQueryParameterService, ITeapotService -{ // ^^^ must inherit the IQueryParameterService interface - pubic bool ShouldThrowTeapot { get; } - - public string Name => "teapot"; - - public override void Parse(KeyValuePair queryParameter) - { - if(bool.Parse(queryParameter.Value, out bool config)) - ShouldThrowTeapot = true; - } -} - -// Startup.cs -services.AddScoped(); // exposes the parsed query parameter to your application -services.AddScoped(); // ensures that the associated query parameter service will be parsed internally by JADNC. -``` - -Things to pay attention to: -* The teapot service must be registered as an implementation of `IQueryParameterService` to be processed internally in the middleware -* Any other (business) logic is exposed on ITeapotService for usage in your application. - - -Now, we could access the custom query parameter service anywhere in our application to trigger a 418. Let's use the resource hooks to include this piece of business logic -```c# -public class TodoResource : ResourceDefinition -{ - private readonly ITeapotService _teapotService; - - public TodoResource(IResourceGraph graph, ITeapotService teapotService) : base(graph) - { - _teapotService = teapotService - } - - public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) - { - if (teapotService.ShouldThrowTeapot) - throw new JsonApiException(418, "This is caused by the usage of teapot=true.") - } - -} -``` \ No newline at end of file diff --git a/wiki/v4/decoupling-architecture.md b/wiki/v4/decoupling-architecture.md deleted file mode 100644 index ef732d2308..0000000000 --- a/wiki/v4/decoupling-architecture.md +++ /dev/null @@ -1,9 +0,0 @@ -# V4 Architecture overview - -We upgraded to .NET Core 3.1. Furthermore, for V4 we have some explaining to do, namely the most notable changes: - -- [Serialization](./content/serialization.md) -- [Query Parameter Services](./content/query-parameter-services.md) -- [Extendability](./content/extendability.md) -- [Testing](./content/testing.md) -- [Deprecation](./content/deprecation.md)