Annotation Interface AssertLegal
Can be used in two ways:
- On methods inside a command or query class to perform legality checks directly
- On properties of a command or query class to delegate legality checks to the annotated property’s class
Method-based usage
Annotated methods are invoked after the aggregate and its entities are loaded usingEntity.assertLegal(Object) or Entity.assertAndApply(Object).
Parameters are injected automatically and may include:
- The command or query object itself
- Any matching entity from the aggregate tree (including parent or grandparent entities)
- Any independently stored
@Modelloaded for the current model commit, either as its value or asEntity<T> - Other framework-specific types like
MessageorUser
Note that empty entities (i.e., those with null values) are not injected unless the parameter is annotated with @Nullable.
Example: Validate entity does not exist yet
@AssertLegal
void assertNew(Issue issue) {
throw new IllegalCommandException("Issue already exists");
}
Example: Validate entity does exist
@AssertLegal
void assertExists(@Nullable Issue issue) {
if (issue == null) {
throw new IllegalCommandException("Issue not found");
}
}
Property-based usage
When placed on a field of a command or query payload (e.g.,@AssertLegal UserDetails details),
the framework will look for @AssertLegal methods or fields within that field’s value.
This enables modular legality checks colocated with the data they validate.
Example
public class UpdateUser {
UserId userId;
@AssertLegal
UserDetails details; // triggers @AssertLegal methods inside UserDetails
}
Return value inspection
If an@AssertLegal method returns a non-null object, Fluxzero will also inspect that return value for further
@AssertLegal methods or properties. This allows for deep, composable validation logic.
For independent Models, return values (including collection elements) are traversed in the
returning method's phase. Nested methods keep the original payload, metadata, user and application parameter
resolvers; injected Models are loaded at the same pinned read boundary and participate in conflict validation.
Model references are selected per parameter from the nearest validator declaring them, then enclosing validators,
then the triggering payload. Explicit nulls and empty collections do not fall back. Association metadata retains
its usual precedence for that parameter, without replacing unrelated selections. Ancestors follow the selected
references. This selection does not replace the original payload supplied to payload parameters.
Null values are ignored. Each object identity is visited once within a payload or Model assertion phase, so shared
references and cycles do not execute its checks repeatedly. Nesting beyond 256 levels is rejected.
Annotated Model fields, including record components, delegate in both phases; methods on their values select
their own afterHandler() timing. A method is invoked only in its declared phase, even when it is a no-arg
accessor. To share one validation object between before and after checks, expose it through an annotated field
rather than relying on a before-method being invoked again after apply. Fluxzero.assertLegal(Object)
performs only immediate checks. Rebase under ACCEPT and event replay do not rerun assertions.
Interaction with intercepted updates
@InterceptApply resolves the effective update or
updates first when current-input checks are disabled. Assertions then run for a retained update, do not run for a suppressed update, and run only for
the replacement when the original update is replaced. Expanded updates are processed in encounter order: each
update's immediate assertions run before its apply methods and see state produced by earlier updates. Assertions
configured with afterHandler() remain deferred until handler completion. For Models, every ordinary,
nested, retained-input and cascading after-handler check sees the complete final state of the atomic Model
operation, including later split outputs and automatic child deletions. Each check retains its own payload,
metadata and user context. A failure rejects the entire operation before commit; this does not combine separate
commands or commits into one transaction.
@InterceptApply(assertCurrent = AssertCurrent.ENABLED) retains checks for that interceptor's input.
Immediate checks run before interception within the commit attempt; retained after-handler checks use final state.
This applies even to suppressed input. DEFAULT enables this from defaults version 2026.10.04, unless
fluxzero.interceptApply.assertCurrent overrides it. Explicit annotation settings win.
Ordering
Cascading rules prevalidate resolved scopes before ordinary assertions; result-dependent routes are checked after the pure apply returns, against the logical before-state. Multiple ordinary legality methods may be invoked. For independently stored models, ordinary assertions declared on the payload run before assertions declared on the model. Within each phase their execution order is determined bypriority(), with higher values taking precedence. Methods with the same priority are invoked in
deterministic order by method name and signature. Immediate assertions see the state before apply; deferred
assertions use the same payload-then-model order against the fully composed state.
Execution timing
By default, checks run immediately during handler execution. You can defer them until after the handler completes (after any @Apply invocations but just before aggregate updates are committed) usingafterHandler().- See Also:
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final intstatic final intstatic final int -
Optional Element Summary
Optional ElementsModifier and TypeOptional ElementDescriptionbooleanDetermines if the legality check should be performed immediately (the default), or when the current handler is done, i.e.: after @Apply and just before the aggregate updates are committed.Class<?>[]Restricts independent-Model assertion methods to assignable payload classes/interfaces; empty means unrestricted.booleanAlso validates mutations of known descendants through enabledParentrelationships.intDetermines the order of assertions if there are multiple annotated methods (or fields on independent Models).
-
Field Details
-
HIGHEST_PRIORITY
static final int HIGHEST_PRIORITY- See Also:
-
LOWEST_PRIORITY
static final int LOWEST_PRIORITY- See Also:
-
DEFAULT_PRIORITY
static final int DEFAULT_PRIORITY- See Also:
-
-
Element Details
-
priority
int priorityDetermines the order of assertions if there are multiple annotated methods (or fields on independent Models). A method with higher priority will be invoked before methods with a lower priority. UseHIGHEST_PRIORITYto ensure that the check is performed first. Methods with the same priority are invoked in deterministic order by method name and signature.- Default:
0
-
cascade
boolean cascadeAlso validates mutations of known descendants through enabledParentrelationships. The declaring Model's own mutations remain subject to this assertion. Defaults to false. Only Model methods opt into cascading; ordinary aggregate assertions retain their existing semantics. All known enabled routes participate, including old and new routes when reparenting. Shared ancestors run once per effective update/phase. Immediate rules see the logical before-state; after-handler rules see final state. Result-bound routes are checked once the pure apply returns. Normal payload and context injection is retained.This is SDK-side validation; unavailable classes and missing parent values stop traversal, and replay does not execute assertions. Deploy the rule to every writer. Read ancestors participate in atomic conflict validation: RETRY reevaluates; configured ACCEPT becomes FAIL for guarded attempts. No new Runtime protocol is required.
- See Also:
- Default:
false
-
allowedClasses
Class<?>[] allowedClassesRestricts independent-Model assertion methods to assignable payload classes/interfaces; empty means unrestricted.- Default:
{}
-
afterHandler
boolean afterHandlerDetermines if the legality check should be performed immediately (the default), or when the current handler is done, i.e.: after @Apply and just before the aggregate updates are committed. For independent Models, this selects method timing; annotated fields delegate to their values in both phases.- Default:
false
-