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.
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. Assertions therefore 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.
If a rule must also hold after an interceptor replaces the original payload, define that rule for the effective replacement or place it in shared/entity-side assertion logic that matches the replacement. An assertion that only matches the original payload is intentionally not invoked after replacement.
Ordering
Multiple legality methods may be invoked. For independently stored models, 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.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
-
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
-