Auto server validation
A name or email can pass local formatting rules and still fail a database-backed rule. Enable autoServerValidate to surface such server-only failures while the user fills out the form. It is optional and defaults to false; ordinary submission always validates on the server when local validation passes.
Enable preflight feedback
Section titled “Enable preflight feedback”This complete component uses the generated UpdateProfile from the validation example. Configure Arc and generate the proxy before using it.
import { CommandForm, InputTextField, useIsCommandExecuting } from '@cratis/arc.react/commands';import { UpdateProfile } from './commands/UpdateProfile';
function SubmitButton() { const isExecuting = useIsCommandExecuting(); return <button type="submit" disabled={isExecuting}>{isExecuting ? 'Saving…' : 'Save'}</button>;}
export function ProfileForm() { return ( <CommandForm command={UpdateProfile} initialValues={{ name: '', email: '' }} validateOn="change" autoServerValidate autoServerValidateThrottle={500} > <InputTextField<UpdateProfile> value={c => c.name} title="Name" /> <InputTextField<UpdateProfile> value={c => c.email} title="Email" type="email" /> <SubmitButton /> </CommandForm> );}After locally valid edits settle, the form calls the generated command’s validate() and displays returned field errors. The validation request uses the command’s resolved route with /validate appended; use the generated route rather than guessing a kebab-case URL. This requires an Arc command endpoint, not an arbitrary JSON validation service.
Validation flow
Section titled “Validation flow”The typing timer is a debounce, despite the prop’s Throttle name: another edit clears the pending timer. The default is 500 ms; a value of zero removes the delay on this path. Choose a longer delay for expensive checks.
The delay is not a global request limiter. With autoServerValidate enabled, initialization/population and eligible blur validation also call validate() and are not delayed by that timer. validateOnInit={false} only hides initial error display; it does not prevent the initialization check. validateOn controls interaction error display, while silent client validation runs on each edit. Do not promise exactly one request per typing burst or per form.
Add a server-only rule
Section titled “Add a server-only rule”First define an actual endpoint, such as UpdateProfile in Backend validation. A standalone DTO and validator alone are not this model-bound endpoint.
The following application integration fragment adds the directory rule beside that example’s command; the frontend form behavior below uses the shared client runtime. On the TypeScript backend, verify the projected rules against its generator validation reference rather than assuming the backend rule is emitted. It compiles alongside UpdateProfile; to run it, supply and register your application’s profile-directory implementation. The allowed-email check must apply your own profile/tenant policy, including allowing an unchanged email for an existing profile.
using System.Threading;using System.Threading.Tasks;using Cratis.Arc.Commands;using FluentValidation;
public interface IProfileDirectory{ Task<bool> IsEmailAllowed(EmailAddress email, CancellationToken cancellationToken);}
public class UpdateProfileValidator : CommandValidator<UpdateProfile>{ public UpdateProfileValidator(IProfileDirectory profiles) { RuleFor(command => command.Name) .NotEmpty().MinimumLength(3).MaximumLength(100); RuleFor(command => command.Email) .MustAsync((command, _, cancellationToken) => profiles.IsEmailAllowed(command.Email, cancellationToken)) .WithMessage("This email cannot be used for this profile."); }}public interface ProfileDirectory { CompletionStage<Boolean> isEmailAllowed(String email);}
public final class UpdateProfileDirectoryRules implements AsyncCommandValidator<UpdateProfile> { private final ProfileDirectory profiles;
public UpdateProfileDirectoryRules(ProfileDirectory profiles) { this.profiles = profiles; }
@Override public Class<UpdateProfile> getCommandType() { return UpdateProfile.class; }
@Override public CompletionStage<List<ValidationResult>> validate( UpdateProfile command, CommandContext context ) { return profiles.isEmailAllowed(command.email()).thenApply(allowed -> allowed ? List.of() : List.of(ValidationResult.error( "This email cannot be used for this profile.", List.of("email")))); }}
@Configurationpublic class UpdateProfileValidation { @Bean public CommandValidator<UpdateProfile> updateProfileDirectoryRules(ProfileDirectory profiles) { return new AsyncCommandValidatorAdapter<>(new UpdateProfileDirectoryRules(profiles)); }}interface ProfileDirectory { suspend fun isEmailAllowed(email: String): Boolean}
@Componentclass UpdateProfileDirectoryRules( private val profiles: ProfileDirectory) : CommandValidator<UpdateProfile> { override val commandType: Class<UpdateProfile> = UpdateProfile::class.java
override suspend fun validate( command: UpdateProfile, context: CommandContext ): List<ValidationResult> = if (profiles.isEmailAllowed(command.email)) { emptyList() } else { listOf( ValidationResult.error("This email cannot be used for this profile.", listOf("email")) ) }}import { CommandValidator, injectable, validator } from '@cratis/arc.core';
export abstract class ProfileDirectory { abstract isEmailAllowed(email: EmailAddress, signal: AbortSignal): Promise<boolean>;}
@validator(UpdateProfile)@injectable(ProfileDirectory)export class UpdateProfileValidator extends CommandValidator<UpdateProfile> { constructor(profiles: ProfileDirectory) { super(); this.ruleFor(command => command.name).notEmpty().minLength(3).maxLength(100); this.ruleFor(command => command.email) .mustAsync((_email, command, signal) => profiles.isEmailAllowed(command.email, signal)) .withMessage('This email cannot be used for this profile.'); }}In C#, keep the EmailAddressValidator from the linked example: it owns the required/format rules, and this replacement validator takes over the command-level ones. Arc’s concept-aware RuleFor unwraps the property, so the three-argument MustAsync overload reads the named command.Email from the command when calling the application service. MinimumLength and MaximumLength are FluentValidation method names.
The shapes genuinely diverge here. Arc.Kotlin’s shared fluent surface is deliberately literal and has no asynchronous or predicate rule, so the JVM sibling is an ordinary CommandValidator whose validate is already a suspending function, added beside the FluentModelValidator that keeps projecting the literal rules to the client. There is also no CancellationToken parameter on the JVM: a suspending validator runs inside Arc’s command coroutine, so cancelling the request cancels what it awaits.
Either way the asynchronous directory rule remains server-side, while the name rules and supported format rules are generated for the client. No minLength, maxLength, or pattern props are supported on the built-in InputTextField.
A server preflight runs pipeline filters and validators but skips the command handler and handler-argument resolution. Validators and filters must avoid unwanted side effects and disclosing sensitive information. A successful check does not reserve an email or enforce uniqueness under concurrent writes: enforce that invariant when the application performs the write as well.
Read the response
Section titled “Read the response”Arc returns a command result with a validationResults array, not a validationErrors dictionary. This is an illustrative failure envelope; the correlation ID and rule message are example values:
{ "correlationId": "34c66385-006a-4ab6-a3f9-cb34c751ec30", "isSuccess": false, "isValid": false, "isAuthorized": true, "hasExceptions": false, "validationResults": [ { "severity": 3, "message": "This email cannot be used for this profile.", "members": ["email"], "state": null, "reason": "rule" } ], "exceptionMessages": [], "exceptionStackTrace": "", "authorizationFailureReason": "", "response": null}A failed preflight has no command response to consume; some server results omit that optional member rather than returning null. CommandForm matches members to fields and displays the first matching message by default. Use an error summary for all messages or results without members.
Limits and operating guidance
Section titled “Limits and operating guidance”isExecutingreports execution, not preflight network activity. CommandForm currently exposes no validation-pending/loading state. Do not label it “Checking availability.”- A previous validity verdict can remain while a new request runs. Internal issue tokens reject results overtaken by later applied validation, but do not guarantee every visible verdict describes the latest edit. For a custom gated UI, use the revision-guarded pattern.
- Preflight may short-circuit on client rules; it does not always reach server authorization. Execution must validate and authorize again.
- The timer reduces traffic; it does not stop abuse. Configure ASP.NET Core rate-limiting middleware/policies or equivalent gateway controls in your application and verify they cover the generated validation endpoint. Arc does not provide the previously illustrated
RateLimitAttribute(MaxRequests, TimeWindowSeconds)API. - Keep lookups indexed and bounded. Cache only with correct tenant/user keys and expiration; stale cached availability is advisory, never write-time enforcement.
- Prefer submit-only validation for expensive endpoints or rules whose answers reveal private data. Handle request failures without treating them as success.