Refuse a save
Updated
Stop a dictionary or document save and show one sentence the user can act on. Name what and where. HTTP 409. This page is the save-refusal recipe, not the whole event-handler catalog and not the translator — Write an event handler, What the user reads when an operation fails.
Note
Refuse in
OnBeforeSave/OnBeforeSaveAsync. After-save hooks run when the row is already written; a cancel there is logged and does not roll the save back. Do not read a second table in a create event — put that lookup on a command.
How to#
1. Owner — the handler in the object's own model. Link 0. Do not call
next. Do not put [ExtensionOf] on that class (ZOCOC004 the other
way). Return EventResult.Cancel(Res.Key) or EventResult.Ok().
The pair is the envelope (Events/<Name>EventHandler.script.json) plus the
user partial (*.cs). Empty templates are normal; fill them.
public partial class CustomerEventHandler : TypedDictionaryEventHandler<Customer>
{
public override Task<EventResult> OnBeforeSaveAsync(
Customer record, bool isNew, EventContext context)
{
if (string.IsNullOrEmpty(record.Name))
return Task.FromResult(EventResult.Cancel(Res.NameRequired));
return Task.FromResult(EventResult.Ok());
}
}
That snippet matches Write an event handler. The
text is a script resource on this script
(Resources tab): fill Value (English), RU, and AR in the same change.
Not a C# literal. Not Developer → Resources. A new key needs a compile
(Res is generated). Changing the text of an existing key is live.
2. Extension on a foreign object. [ExtensionOf("Name")] on the class.
Call next first, then cancel. Missing next is ZOCOC001. Missing
[ExtensionOf] is ZOCOC004. Work after next() sees what the owner already
accepted — Wrap with Chain of Command.
Example only — there is no CustomerWmsEventHandler and no WmsCode on
product Customer. The field lives in your model
(Add a field on a foreign dictionary).
It is not a property on the generated Customer class. Read the bag of
this save (Record(context)), not a second GetByIdAsync.
[ExtensionOf("Customer")]
public partial class CustomerWmsEventHandler : TypedDictionaryEventHandler<Customer>
{
public override async Task<EventResult> OnBeforeSaveAsync(
Customer record, bool isNew, EventContext context)
{
var prior = await next(record, isNew, context);
if (!prior.Success) return prior;
var bag = Record(context);
var code = bag.TryGetValue("WmsCode", out var raw) ? raw as string : null;
if (string.IsNullOrWhiteSpace(code))
return EventResult.Cancel(Res.WmsCodeRequired);
return EventResult.Ok();
}
}
Res.WmsCodeRequired is a key on this script. Write a sentence the person
at the screen can act on: "Fill in WMS code on the customer before saving."
A toast that says "invalid" on a card with twenty fields is not a refusal.
3. A service may throw instead. throw new BusinessRuleException(Res.Key)
from a service the handler (or a command) calls. Older scripts throw
InvalidOperationException; when our code throws it, it still counts as a
refusal. A NullReferenceException or a foreign-key fault becomes a generic
sentence — check the field and refuse with words —
What the user reads when an operation fails.
The platform turns EventResult.Cancel into BusinessRuleException. The
global handler answers HTTP 409 with that text. A command that refuses
without throwing is 200 with success: false. A crash is 500.
4. Bump modelVersion in the same change (patch: a code fix). You wrap a
foreign object from your model — bump yours.
Bump the model version.
Warning
Never
HttpClientfrom a handler that is deciding a save. Outbound I/O is a channel name — Send an outbound call.A service is not the running script. A key bound only to the service's own envelope is not found;
Res.Keyreturns the key name. Put the sentence on the event that shows it, or leave a truly shared key unscoped — Add a script resource.
What's not there yet#
UserErrors.Duplicate still shows Russian unique-value sentences, for example
Запись со значением «X» уже есть: значение должно быть уникальным. The
user's interface language does not change them. Same family as the other
translated fault sentences —
What the user reads when an operation fails.
There is no HttpClient in a script. There is no per-script namespace in
Res. EventResult.Cancel carries no stack: the code button points at the
script that started the save, not at the return that refused it.