Additional data
Additional data lets each tenant add its own fields to an entity, at runtime, without changing the metadata or regenerating the cluster. A tenant administrator defines the fields (name, type, constraints, display settings) in a screen of the NeosAdditionalData module. The values are then read and written through the Web API, queried with OData, and entered in the grids and forms of the application.
Nothing changes for an entity which does not enable additional data, nor for a cluster which does not reference the module.
Enable additional data
Three steps make the additional data of an entity available:
- Choose the storage mode of the cluster (optional,
Jsonby default). - Enable additional data on the entity.
- Reference the
NeosAdditionalDatamodule, which stores the definitions and provides their screen.
Choose the storage mode
The storage mode is set once for the whole cluster, in its configuration file:
# Storage of the additional data (custom fields) of the entities enabling them: Json (default) or Eav
AdditionalData:
StorageMode: Eav
| Mode | Storage | Suited to |
|---|---|---|
Json |
A JSON column named NeosAdditionalData, added to the table of each entity enabling it. |
Few fields per entity, values mostly read with their item. |
Eav |
A single entity-attribute-value table named $NeosAdditionalDataValue for the cluster. |
Many fields, or fields frequently filtered, sorted, and grouped across the items. |
Important
In Eav mode, the values are attached to the primary key of the entity, which must be made of integer or text columns. An entity view keyed by an alternate key is supported: the values still follow the primary key of its entity. The generation reports an error for an entity view whose key cannot be used.
Warning
In Eav mode, the values are stored outside the row of the entity: saving an item whose only changes are in customFields does not renew its RowVersion. The optimistic concurrency check does not detect two users changing only the additional data of the same item at the same time, and the last save wins.
Enable the entity
In Neos Studio, check Allow additional data in the form of the entity. It writes the following key in the YAML file of the entity:
AllowAdditionalData: true
Each entity view of the entity then exposes a customFields property, and each UI view based on one of these entity views loads the definitions of the fields when it is initialized.
Reference the module
Reference the NeosAdditionalData module of the TenantManagement cluster in the cluster, and add a dependency to it in the module which displays the fields in its forms. The module brings:
- the Additional data definitions menu item, which opens the screen of the definitions (
NeosAdditionalDataDefinitionUI), - the Additional data function (
AdditionalData), which grants access to this screen: give it to the administrators of the tenant who manage the fields, - the
neos-additional-data-slotUI component, described below.
Without the module, no definition exists, and the entities enabling additional data behave as if they had no field.
Define the fields
A definition describes one field of one entity, for the current tenant.
| Setting | Description |
|---|---|
| Entity name, name | The entity and the field name. The name is an identifier (letters, digits, _) which cannot be the name of a property of the entity. |
| Caption | The caption displayed in grids and forms, in each language. |
| Type, multiple values | String, Integer, Long, Decimal, Boolean, Date, DateTime, Choice, or LocalizableString, holding one or several values (except a Boolean or LocalizableString field). |
| Required | A value must be entered on creation, and before saving an existing item which has none. The field must then be shown in forms, or in grids when it holds one value or choices. |
| Maximum length, minimum, maximum | Constraints on the length of a text or the range of a number. |
| Choice values | The allowed values of a Choice field, one per line. |
| Format | The display format, for example a date or number format string. |
| Show in grids, grid position, visible | Whether the field is a grid column, its position among the additional columns, and whether the column is visible by default (a hidden column stays available in the column chooser). |
| Show in forms, form position | Whether the field is displayed by the slot component, and its position. |
| Active | Withdraws the field without deleting its values. |
The values are stored under the field name, so the entity, the name, the type, and the multiple values setting cannot be changed once the field exists. A name stays reserved even after the field is withdrawn.
Withdraw, reactivate, and delete a field
The Withdraw action of the definitions screen clears Active: the field disappears from the Web API, OData queries, grids, and forms, but its values are kept. The Reactivate action brings the field back with its values.
The standard deletion of a definition is refused. The Delete action deletes a field permanently, with all its values and in a single transaction, through the DeleteNeosAdditionalDataDefinition server method (DELETE on the additional-data/definitions/{id} route of the module).
Read and write the values
The values of an item are held by its customFields property, an object whose keys are the field names in camel case:
{
"id": 1,
"name": "Notebook",
"customFields": {
"color": "Red",
"stock": 12,
"organic": true,
"tags": ["school", "paper"]
}
}
Writing merges the values instead of replacing them:
- a field absent from
customFieldskeeps its value, so a partialPUTnever loses the other fields, - an explicit
nullclears the value, - the values of a withdrawn field are never modified.
The server checks the type and the constraints of the fields received on each write, and refuses the creation of an item without a value for a required field. An unknown field is refused.
The $neos/metadata/{entityName}/additional-data endpoint returns the active definitions of an entity for the current tenant, with their caption in the language of the request. Any user of the application can read them, since the screens need them to display the fields. The definitions are cached by the server and refreshed as soon as a definition changes.
Note
The OpenAPI description of the entity views declares customFields as a free-form object: the fields are only known at runtime, for each tenant.
Use the values in server rules
Entity view rules
Each entity view of an entity enabling additional data has a CustomFields property, which holds the values of the active fields of the item, keyed by field name (as defined, not in camel case). A field without value is absent. The values are typed:
| Field type | Value type |
|---|---|
String, Choice |
string |
Integer |
int |
Long |
long |
Decimal |
decimal |
Boolean |
bool |
Date |
DateOnly |
DateTime |
DateTime |
LocalizableString |
LocalizableString |
| Field holding several values | List<object> of the values above |
The values are available:
- in the
Retrievedrules, the items being read with their values, - in the validation,
Saving, andSavedrules of a save made through the Web API, the values sent incustomFieldsbeing already merged with the stored ones.
/// <inheritdoc/>
public Task OnSavingAsync(ISavingRuleArguments<IProductView> args)
{
foreach (IProductView product in args.CreatedAndModifiedItems)
{
if (product.CustomFields?.TryGetValue("Stock", out object? stock) == true && stock is int quantity && quantity == 0)
{
product.Name += " (out of stock)";
}
}
return Task.CompletedTask;
}
The rules read the values but do not write them: a value changed in CustomFields by a Saving rule is not saved. The values are written through the customFields property of the Web API, by the grids and forms or by an API client. In a Retrieved rule, a change only affects the returned items.
Entity rules
The entity class does not expose the values as typed properties. In Json storage mode, its AdditionalData property holds them as stored in the JSON column, keyed by field name. In Eav mode, the values are not held by the entity. Read the values in the rules of an entity view instead.
Query with OData
A field is addressed in OData through the customFields container. Its name is not case-sensitive:
| Clause | Example |
|---|---|
$filter |
customFields/Color eq 'Red' and customFields/Stock gt 10 |
$orderby |
customFields/Stock desc |
$select |
Id,Name,customFields/Color, or customFields for every queryable field |
$apply grouping |
groupby((customFields/Color), aggregate(customFields/Stock with sum as Total)) |
| Navigation | Customer/customFields/Tier eq 'Gold' |
A field holding several values, or a LocalizableString field, is stored and returned with its item, but cannot be used in a filter, a sort, a grouping, or a $select clause.
Long texts are subject to the limits of the database:
- In
Jsonmode on SQL Server and Oracle, a filter, a sort, or a grouping reads a text of more than 4,000 characters as empty:customFields/Note eq '...'does not find such a value, although the item still returns it in full. Give a maximum length of 4,000 characters or less to a text field meant to be queried, or choose theEavmode. - On Oracle, a sort or a grouping on a text field only compares its first 2,000 characters.
Display the fields
Grids
The grid of a UI view whose entity enables additional data adds a column for each field shown in grids, after the columns of the entity view, in the order of their grid position. The columns can be sorted, filtered, grouped, hidden, and moved like the other columns, and the custom views keep them.
Each column is edited with the editor of its type: a drop-down list for a Choice field, a multiple selection for several choices. A field holding several values which are not choices is displayed as a list and edited in the form.
When the columns to load are restricted (a property whose ShouldLoad is set to false in the Retrieving rule), the request only selects the fields displayed in the grid or in the form.
Forms
Place the neos-additional-data-slot UI component in the form of the UI view:
<vertical-layout>
<form-field property-name="Id" />
<form-field property-name="Name" />
<neos-additional-data-slot />
</vertical-layout>
It displays the fields shown in forms, in the order of their form position, each with its caption and the input of its type: text, number, date, date and time, check box, drop-down list for a choice, multiple selection, list of values, or localizable text. The required fields are marked, and the constraints are checked on the field before saving, an integer field being also limited to the range of its type. A required field must be entered in a new item, and in an existing item without value, for example an item created before the field was defined, before it can be saved.
Show a tab only when it has fields
The HasAdditionalDataFormFields property of the view model is true when at least one field is shown in forms. To display the fields in a dedicated tab, hidden when the tenant has defined no such field, bind the visibility of the tab to a computed of the UI view returning this property:
<tabs>
<tab-item caption="Product">
<form-field property-name="Name" />
</tab-item>
<tab-item caption="Additional data" visible="@Computeds.HasAdditionalData">
<neos-additional-data-slot />
</tab-item>
</tabs>
The HasAdditionalData computed, of type bool, has the getter return HasAdditionalDataFormFields;. The property is false while the definitions are not loaded, and the tab appears as soon as they are.
Client code
Each field is a property of the view model, named customFields.{Name}, which client code can read like any other property. The view model also exposes the definitions:
IReadOnlyList<AdditionalDataDefinition> definitions = await LoadAdditionalDataDefinitionsAsync();
foreach (AdditionalDataDefinition definition in definitions)
{
IViewModelProperty property = GetProperty("customFields." + definition.Name);
// property.IsCustomField is true
}
The definitions are loaded once, when the view model is initialized and before the custom view is restored, so AdditionalDataDefinitions already holds them in the Initialized rule.