ZuloOne
English
Sign in
Menu

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 HttpClient from 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.Key returns 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.

Where next#

Was this page helpful?