Database Persistence Rules

Writing C# scripts within the Struktural pipeline requires an understanding of how Entity Framework Core (EF Core) Change Tracking interacts with the ephemeral context of an HTTP request.

Standard Pipeline (No explicit saving needed)

When a script runs during a standard CRUD operation (e.g., saving a Form in the UI), the framework manages the transaction automatically.

If you are in BeforeCreate or BeforeUpdate, any changes you make to the Entity object will be persisted automatically by the surrounding pipeline. Do NOT call await Db.SaveChangesAsync(); in these hooks.

// In BeforeCreate.cs.script
Entity.Total = Entity.Quantity * Entity.UnitPrice;
Entity.Status = "Draft";
// End of script. The pipeline will call SaveChanges() for you.

Out-of-Band Modifications (Explicit saving required)

If you modify the database outside the primary execution target, you must manually persist those changes. This applies to:

  1. Custom Actions: The action is triggered by a button, not a "Save" form submission.
  2. Modifying Related Entities: If you update a completely different entity during AfterUpdate.

The SaveEntityAsync Helper (CRITICAL)

Because HTTP contexts are recycled and entities might be detached or tracked ambiguously, calling the standard Db.Update(Entity) followed by Db.SaveChangesAsync() can cause Change Tracker collisions or throw exceptions about multiple tracked instances.

To safely persist records outside the standard pipeline, use the provided extension method: await Db.SaveEntityAsync(record);.

This helper detaches conflicting instances, deep-clones the object safely, applies the state modification, saves the changes, and synchronizes the primary keys and Audit Trails automatically.

// In Invoice_Action_Approve.cs.script
if (Entity.Status == "Draft") 
{
    Entity.Status = "Approved";
    
    // MANDATORY: Explicitly save because CustomActions do not auto-save.
    // Use the safe helper instead of Db.SaveChangesAsync()
    await Db.SaveEntityAsync(Entity); 
    
    Ui.ShowToast("Invoice Approved", "success");
}

Note: If you are inserting a brand new child collection (One-to-Many) alongside the parent, pass the deepSave: true flag to the helper: await Db.SaveEntityAsync(Entity, deepSave: true);.