Table of Contents

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:

  1. Choose the storage mode of the cluster (optional, Json by default).
  2. Enable additional data on the entity.
  3. Reference the NeosAdditionalData module, 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-slot UI 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 customFields keeps its value, so a partial PUT never loses the other fields,
  • an explicit null clears 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 Retrieved rules, the items being read with their values,
  • in the validation, Saving, and Saved rules of a save made through the Web API, the values sent in customFields being 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 Json mode 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 the Eav mode.
  • 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.

See also