Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Special attention should be called to the custom
parameter of all NgRx Auto-Entity generic actions. This parameter is optional and is always the last parameter of any action constructors. The availability of custom criteria for all actions is critical to supporting more advanced use of Auto-Entity, more complex API calls, support for hierarchical API structures, etc.
Sending custom criteria is entirely optional, but is an option for every action. This includes not only loads, but also CURD actions as well. If the set of entities to be retrieved, updated, replaced or deleted must be restricted by criteria beyond simply a uniquely identifying key, then custom criteria should be used.
Custom criteria should also be used to identify parent keys or relation keys in hierarchical API paths. Many REST APIs often relate entities hierarchically by nesting child entities under the paths of parent entities:
GET
/api/v1/customers/:customerId/orders
Retrieves all the orders placed by the specified customer
Name | Type | Description |
---|---|---|
In the case of the above example REST API, the customerId
is not a unique identifying key of the entity Order
. As such it would be inappropriate to specify the customerId as the key in a Load
action. Instead, the customerId
should be passed as part of custom criteria:
The custom criteria with customerId
will be made available as the criteria
parameter to the corresponding loadAll
entity service method down the line, allowing you to make a proper call to the API:
Some times you may wish to retrieve restricted sets of entities based on criteria. Perhaps by date ranges. This is another use case for criteria, the fundamental case. If we expand our above API with additional custom query string parameters for the various order related dates:
GET
/api/v1/customer/:customerId/orders?datePlaced&dateFulfilled&hasDateFullfilled
Retrieves all the orders placed by the specified customer, filtered by optional criteria
We might handle these optional criteria with a more advanced implementation of our Entity service for Orders. First, our initial dispatch:
And our custom loadMany
implementation in our entity service:
With custom criteria, we can handle any kind of backend API, even complex ones with custom and dynamic parameters. Note the use of loadMany
here instead of loadAll
...this is an important distinction with partial data loads vs. full data loads, which I'll be going into more detail on in the next section.
Name | Type | Description |
---|---|---|
Name | Type | Description |
---|---|---|
customerId
number
The unique id of the Customer who's Orders are to be retrieved
customerId
number
The unique id of the Customer who's Orders should be retrieved
datePlaced
string
ISO start date of order placement
dateFullfilled
string
ISO start date of order fullfillment
wasFullfilled
boolean
Flag indicating whether orders should be fullfilled or not
NgRx Auto-Entity offers many ways to load data. The most common of these will usually be the single, all and many loads. A single entity load is pretty strait forward, requiring that you provide a model type and entity key to the Load
action. This will retrieve the single entity uniquely identified by the specified key and merge it into the state for that entity.
There are several terms I will use throughout this documentation when referring to what kind of changes are performed on state as a result of an action.
Merged: State that is merged will add or update existing entities in state with entities of the action being reduced
Replaced: State that is replaced will drop any existing state in favor of the entities of the action being reduced
Concatenated: State where the entities of the action are simply added to whatever is already in state
Loading all entities of a given type is even more strait forward, as you simply provide the model type to the LoadAll
action. This will retrieve every entity of a given type and replace it in the state for that entity.
The third common way of loading entities is to load many entities. This is also strait forward and relies on providing at least a model type to the LoadMany
action. This will retrieve entities and merge them into the state for that entity.
Note that when the behaviors of actions are described here, this is the expected behavior for a given action. Since implementation of Entity services is still the responsibility of the developer, it is up to the developer top ensure they conform to the expectations of each they are handling in their Entity services.
For many loads, you may need to specify criteria other than a primary or uniquely identifying key. All NgRx Auto-Entity actions support optional custom criteria that may be used for this purpose.
Trifurcation Avoidance Paradigm
The primary interface for an application to interact with state in @ngrx is actions. Actions encapsulate intent, along with the information necessary to perform the requested intent. Actions are one of the key sources of "boilerplate" code with standard @ngrx, and one of the primary areas we aim to simplify with Auto-Entity.
With @ngrx, defining actions tends to require defining action types and action classes "in triplicate", as most actions, particularly entity actions, usually require an "initiator" paired with "result" actions. Initiators are dispatched to request that "some action be performed", while results are dispatched in order to denote that "a requested action completed" with success or failure. Actions, therefor, tend to trifurcate
in their actual implementation.
One,All, Many, Pages, Ranges...
The generic actions in the NgRx Auto-Entity library cover all the CRUD bases, as well as a range of loading options, plus some extra useful "utility" actions to cover other common state-related entity tasks. First up, we provide several options for loading data:
Load: one entity at a time
LoadAll: every entity all at once
LoadMany: lots of entities in arbitrary bunches
LoadPage: entities in discrete bunches
LoadRange: entities in sequential bunches
Each load action is imbued with unique semantics and behavioral implications. For example, dispatching a LoadAll
action implies that you wish to replace any previously existing entity state for the given model with whatever set of entities is retrieved by the entity service for that model.
Dispatching LoadMany
on the other hand implies that you wish to keep any previously existing entity state for the given model, and merge in whatever set of entities is retrieved. Similarly, Load
will also merge the retrieved entity into any previously existing state.
Further, pages are semantically different than ranges. A page is a discrete slice of a set of entities with a distinct beginning and end. A range on the other hand is a sequential slice of a set of entities that ultimately form a continuous range. When dispatching LoadPage
the implication is that you wish to replace any previously existing state for the given model.
The explicit use case here is when the entire set of all entities for a given model is simply too large to fit in memory in a browser (i.e. you may have tens of thousands...millions...billions of records in a database.) Standard case for tables with paging controls.
When dispatching LoadRange
on the other hand, the implication is that you wish to join newly loaded ranges of entities onto existing state for the given model. The primary use case here is infinite scrolling, where additional entities are added to previously loaded entities in response to continued vertical scrolling or horizontal panning by a user.
Current Complexity with Action Definitions
A standard approach to implementing actions with @ngrx requires defining an enumeration of action types, which map a code identifier to a string, implementation of an action class that derives from Action
and the concatenation of each action type into an action union that allows proper implementation of a reducer function to reduce actions and the information they contain into your state.
This is a lot of work required just to add the ability to create an entity. Not only do you need the ability to handle the initial create request, but also deal with the success or failure of that request, at the very least. So each "action" generally tends to trifurcate, and thus the action types and action union code trifurcates as well.
Note: The most recent release of NgRx, version 8, has introduced some utility functions that can help reduce some of the "boilerplate" nature of implementing actions, effects and reducers. These new functions are a welcome improvement over prior versions of NgRx...however, we do believe they still fall short of providing the kind of simplified, rapid development experience Auto-Entity provides.
Once actions are defined, one may then dispatch them using the @ngrx store. This is usually done within Angular container components:
For actions to actually do anything, however, you need more. Your work does not end here. You still need effects, and reducers, to make any of these dispatched actions actually perform useful work and update state.
In addition to our "standard" loading actions for single, all and many entity retrievals, we also provide two additional loading actions: LoadPage
and LoadRange
. You may wonder why both, and again it boils down to semantics, behavior. Both are necessary to support different kinds of data loading behavior, as will become evident here.
Loading of paged data is a common practice that dates back many years, in fact decades. It is a fundamental kind of data load, and extensive research has been done as well as dedicated features to support paged data have been integrated into data storage solutions.
Loading pages of data, rather than loading complete sets of data, is often essential. This is especially true in the modern world where we frequently must acquire and work with immense datasets that can number in the millions, billions...even trillions of records. With such numbers, it should be obvious that loading all entities for such datasets would be problematic at best, assuming it was even possible in the first place. Large datasets will usually consume immense space, including immense bandwidth and immense amounts of memory in a browser.
When loading a page, any previously loaded entities for this type will be replaced in by the new page of entities retrieved. This ensures that bandwidth and browser memory space is used efficiently, and that memory is not wasted trying to store an ever-growing number of entities as a user cycles through pages in your UI.
Loading pages of data one at a time with NgRx Auto-Entity is quite easy. Simply use the LoadPage
action, or the loadPage
method on your entity facades, along with the Page
parameter:
When implementing your entity service, you will have access to the Page
object provided in the initial action, which may be leveraged in your API call:
Note the use of pipe()
on the http
call here, and the transformation of the response. Also not the return type of the loadPage
method. NgRx Auto-Entity requires additional meta-data about which page was loaded as well as the total number of entities that may be paged through, in order to store that information in state and make it available to components and your UI (for proper rendering of paging controls, for example).
It is important to understand the nature of the data that must be returned by the loadPage
method in your entity services. The return type is IEntityWithPageInfo<TModel>
which is defined as so:
The child type of the property pageInfo
, of type IPageInfo
is defined as so:
One way or another, you must be able to provide the above information for each paged load. You may simply prefer to reflect back the original page information sent in for the IPageInfo
page property, however usually the totalCount
must be returned by the server in one way or another. It need not necessarily be on the same call, an additional call may be performed to retrieve the total count of entities:
Another possibility, although potentially not canonical standard depending on exactly how you implement it, might be to use the HTTP Content-Range
header in your responses to support additional metadata about page info, without having to include it in the body of the response. This header is normally used with a unit of bytes, however technically speaking the units can be arbitrary (such as, say, records
or entities
):
Radical Simplification with Reusable Action Libraries
NgRx Auto-Entity provides a relatively simple solution to the action triplet trifurcation conundrum. Make commonly-implemented actions reusable! If there ever was a use case for generics, CRUD entity actions would seem to be as sublimely perfect a case as ever possible. By making actions generic, this allows a simple library of standard CRUD actions to be implemented that could then be easily reused by any number of unique entities.
Generic actions are the primary interface with which an application interacts with NgRx Auto-Entity. No need to implement the same pattern of three actions again and again for each and every entity! We have provided all the necessary actions for you. You simply need to dispatch them!
Aside from no longer having to implement your own actions, everything else should be business as usual. Your controllers, when dispatching generic actions from Auto-Entity directly, should look quite familiar.
Each generic action's constructors require a standard set of parameters, and some actions may require unique parameters to perform the necessary function. Action constructors are as follows: