Actions, validation and workflows
Vox-owned entities are changed through actions rather than unrestricted JSON updates. An action describes an intent—such as changing a product name, scheduling a campaign or closing a market—and its handler decides whether that intent is valid and how it changes the entity.
For ordinary editable properties, Vox generates create, update and delete actions from the entity model. Applications add custom actions where a change represents a business operation or has rules that a generic field editor cannot express.
Generated and custom actions
The generated actions make straightforward editing inexpensive. A field group gets an update action, and an index-backed relation gets set, add and remove actions. Attributes can replace those defaults with custom commands or remove deletion entirely.
A campaign lifecycle is a good example. Scheduling is one operation even though it changes dates and status together:
[VoxUpdateFieldGroups(
VoxFieldGroupDefinition.DefaultName,
nameof(Campaign.Schedule))]
[Display(Name = "Schedule campaign")]
[VoxConfirmation("Schedule this campaign with the selected interval?")]
public class ScheduleCampaign : IVoxUpdate<Campaign>
{
public required DateTime StartsAtUtc { get; set; }
public required DateTime EndsAtUtc { get; set; }
}
Its handler receives both the command and the current entity. It can reject an invalid transition, update several
field groups, change relations and append an entity-log entry as one atomic change. [VoxUpdateFieldGroups] and
[VoxUpdateRelations] declare the parts an action is allowed to change, so its concurrency and permission boundaries
remain visible in the model.
Custom create and delete commands use the same model. [VoxCreatedBy], [VoxUpdatedBy] and [VoxDeletedBy] connect
an entity to the commands that own those operations. [VoxNoDelete] declares that deletion is not part of the
entity's lifecycle.
Validation at different levels
Vox validates the complete candidate entity before saving it, whichever action produced it. Validation has three complementary levels:
- Data annotations describe local constraints such as required values, ranges and string lengths.
IVoxValidator<TEntity>implements rules that need the complete entity, its previous version or other data.- An action handler rejects an operation whose meaning is invalid in the current state.
For example, a campaign validator can check that the start precedes the end and that newly selected markets exist. The lifecycle handler can separately reject an attempt to activate a campaign without products or a discount. The first rule is true of every campaign version; the second belongs specifically to the activation operation.
External entities are not validated by Vox because their source system owns their truth. Import handlers can still validate an incoming payload or reject an import, but Vox does not reinterpret the source's entity as locally owned.
Uniqueness
[VoxUnique] defines a database-backed uniqueness rule for a property or a combination of property paths:
[VoxUnique(nameof(Name), nameof(City), Name = "NameInCity")]
public class Store : IVoxEntity
{
public required string Name { get; set; }
public required string City { get; set; }
}
String comparisons ignore case by default. Drafts may temporarily conflict with published content while they are being prepared, but the rule is enforced when a version is published.
Concurrency and atomicity
Every update is based on a version. A stale command fails when a part it changes has moved on, rather than silently overwriting another editor's work. Independent field groups and index-backed relations have independent revisions, so unrelated changes can still be merged safely.
A client can submit several actions in order and have Vox save them as one change. Either every action and relation mutation succeeds, or none does. This lets a metadata-driven editor keep small, meaningful commands while presenting one Save button for a larger form.
Actions use the same save targets as other Vox-owned changes. They can publish immediately, update an ordinary draft, or contribute a version to a content release. Validation and business rules run before any of those versions is stored.
Why actions matter
The action model keeps domain decisions out of controllers and user interfaces. The Admin UI, another application and an automated agent can all discover the same commands and validation metadata, but none needs to duplicate the rules that make a change valid.
See Drafts and publishing, Users, roles and permissions and Editor metadata and selections.