Persisting Data in Acumatica Events – Part 5: Case Studies & Troubleshooting
Welcome to the final part of this series. Over the previous four parts, we've covered everything from the fundamentals of PXCache to complex production patterns. In this concluding part, we'll examine real-world case studies from Acumatica implementations, common troubleshooting scenarios, and lessons learned from the field.
⚡ Key Insight: The best way to master data persistence in Acumatica is to study how experienced developers have solved real-world problems and learn from their challenges and solutions.
Case Study 1: Performance Crisis in a Multi-Company Environment
A large distribution company with multiple legal entities experienced severe performance degradation during order processing. The system would freeze for 30-60 seconds when saving an order.
The Problem
The custom RowPersisting event handler was querying the database for each order line to validate inventory across all companies. With a typical order having 50-100 lines, this resulted in 50-100 database round trips per order.
// ❌ Original inefficient code
protected virtual void _(Events.RowPersisting<SOLine> e)
{
var line = e.Row;
// BAD: Query executed for EACH line
var inventory = PXSelect<InventoryItem,
Where<InventoryItem.inventoryID, Equal<Required<InventoryItem.inventoryID>>>>
.Select(Base, line.InventoryID); // 1 round trip per line!
if (inventory?.AvailableQty < line.Quantity)
{
throw new PXException($"Insufficient inventory for {inventory.InventoryCD}");
}
}
The Solution
The solution was to batch the inventory queries and use a PXDatabase select to retrieve all inventory data in a single round trip.
// ✅ Optimized code
private List<InventoryItem> _inventoryCache;
protected virtual void _(Events.RowPersisting<SOLine> e)
{
var line = e.Row;
// Lazy load inventory cache for this transaction
if (_inventoryCache == null)
{
var inventoryIDs = GetInventoryIDsForOrder();
_inventoryCache = PXSelect<InventoryItem,
Where<InventoryItem.inventoryID, In<Required<InventoryItem.inventoryID>>>>
.Select(Base, inventoryIDs.ToArray())
.Cast<InventoryItem>()
.ToList();
}
var inventory = _inventoryCache.FirstOrDefault(i => i.InventoryID == line.InventoryID);
if (inventory?.AvailableQty < line.Quantity)
{
throw new PXException($"Insufficient inventory for {inventory.InventoryCD}");
}
}
✅ Lesson Learned: Always batch database queries in event handlers. Avoid database round trips inside loops. Use PXDatabase for bulk operations when event logic isn't required.
Case Study 2: Deadlock During Order Approval
A manufacturer experienced periodic deadlocks when approving orders. The system would hang, requiring manual intervention to restart the approval process.
The Problem
The custom Persist override was creating audit records and updating inventory in the same transaction, using the same graph instance. This caused lock escalation and deadlocks with other concurrent operations.
// ❌ Problematic code
[PXOverride]
public void Persist(Action baseMethod)
{
// BAD: Creating records in the same graph/transaction
var order = Base.Orders.Current;
// Creating audit record in same graph
var audit = new OrderAudit { OrderNbr = order.OrderNbr, Status = "Approved" };
Base.Audit.Insert(audit);
// Updating inventory in same graph
UpdateInventory(order);
baseMethod(); // All changes saved together
}
The Solution
The solution was to use a separate graph instance for audit records and update inventory in a background process after the transaction was committed.
// ✅ Optimized code
[PXOverride]
public void Persist(Action baseMethod)
{
// Use a different graph for audit (created in baseMethod)
baseMethod();
}
protected virtual void _(Events.RowPersisted<SOOrder> e)
{
if (e.TranStatus != PXTranStatus.Completed) return;
// Use a separate graph for audit
using var auditGraph = PXGraph.CreateInstance<AuditGraph>();
var audit = new OrderAudit { OrderNbr = e.Row.OrderNbr, Status = "Approved" };
auditGraph.Audit.Insert(audit);
auditGraph.Actions.PressSave();
// Schedule inventory update for background processing
QueueInventoryUpdate(e.Row);
}
✅ Lesson Learned: Avoid creating records in Persist overrides that use the same graph. Use RowPersisted with a separate graph instance or schedule long-running operations for background processing.
Case Study 3: Silent Data Corruption in Shipment Processing
A logistics company discovered that shipment quantities were being silently modified during processing, causing inventory discrepancies and customer complaints.
The Problem
The custom RowPersisting handler was modifying the shipment line quantity without raising the appropriate events, leading to inconsistent data in dependent caches.
// ❌ Problematic code
protected virtual void _(Events.RowPersisting<ShipmentLine> e)
{
var line = e.Row;
if (line.ShippedQty > line.OrderQty)
{
// BAD: Direct field assignment - events not raised
line.ShippedQty = line.OrderQty; // No events triggered!
}
}
The Solution
The solution was to use PXCache.SetValueExt which properly raises events and updates dependent caches.
// ✅ Corrected code
protected virtual void _(Events.RowPersisting<ShipmentLine> e)
{
var line = e.Row;
if (line.ShippedQty > line.OrderQty)
{
// GOOD: SetValueExt raises events and updates status
e.Cache.SetValueExt<ShipmentLine.shippedQty>(line, line.OrderQty);
// This ensures all dependent logic runs correctly
}
}
⚠️ Critical Lesson: Never assign values directly in event handlers unless you're absolutely certain no dependent logic exists. Always use SetValueExt to ensure events are raised and caches are updated properly.
Case Study 4: Upgrade Failure with Custom Fields
An ISV solution failed during upgrade from 2025 R2 to 2026 R1 because custom fields didn't have the Usr prefix.
The Problem
Custom fields were added to standard DACs without the Usr prefix, and the customization project used direct SetValue calls without proper event handling.
// ❌ Problematic code
public class SOOrderExt : PXCacheExtension<SOOrder>
{
// BAD: Missing Usr prefix - may be removed during upgrade
[PXDBDecimal(2)]
public virtual decimal? CustomDiscount { get; set; }
}
// In event handler
protected virtual void _(Events.RowUpdating<SOOrder> e)
{
// BAD: Direct assignment without events
e.Row.GetExtension<SOOrderExt>().CustomDiscount = CalculateDiscount(e.Row);
}
The Solution
The solution was to add the Usr prefix to all custom fields and use SetValueExt for all field updates.
// ✅ Corrected code
public class SOOrderExt : PXCacheExtension<SOOrder>
{
// GOOD: Usr prefix ensures upgrade safety
[PXDBDecimal(2)]
[PXUIField(DisplayName = "Custom Discount")]
public virtual decimal? UsrCustomDiscount { get; set; }
}
// In event handler
protected virtual void _(Events.RowUpdating<SOOrder> e)
{
// GOOD: SetValueExt raises events and maintains data consistency
e.Cache.SetValueExt<SOOrderExt.usrCustomDiscount>(
e.Row,
CalculateDiscount(e.Row));
}
✅ Lesson Learned: Always use the Usr prefix for custom fields to ensure upgrade safety. Always use SetValueExt instead of direct assignment to maintain data consistency and event integrity.
Troubleshooting Common Errors
Error 1: "Object reference not set to an instance of an object" in CstPageData.Save
This error occurs when a customization package contains ASPX page customizations for screens delivered by another package, and the prerequisite package hasn't been published yet.
Solution: Publish prerequisite packages before importing dependent ones. The import order matters in 2026 R1.
Error 2: "Cannot insert duplicate key" during RowPersisted
This error typically occurs when creating related records in RowPersisted without checking if they already exist.
Solution: Always check for existing records before inserting:
protected virtual void _(Events.RowPersisted<SOOrder> e)
{
if (e.TranStatus != PXTranStatus.Completed) return;
var order = e.Row;
// Check if record already exists to avoid duplicates
var existing = PXSelect<OrderAudit,
Where<OrderAudit.orderNbr, Equal<Required<OrderAudit.orderNbr>>>>
.Select(this, order.OrderNbr);
if (existing != null) return;
// Create audit record
}
Error 3: "System.StackOverflowException" in Event Handlers
This error occurs when event handlers modify data in a way that triggers the same event recursively.
Solution: Use a flag to prevent recursion:
private bool _isProcessing = false;
protected virtual void _(Events.RowUpdating<SOOrder> e)
{
if (_isProcessing) return;
_isProcessing = true;
try
{
var order = e.Row;
if (order.DiscountPercent > 0)
{
e.Cache.SetValueExt<SOOrder.discountAmt>(
order,
order.TotalAmount * order.DiscountPercent / 100);
}
}
finally
{
_isProcessing = false;
}
}
Final Best Practices Checklist
✅ Use Usr Prefix for Custom Fields
Always prefix custom fields with "Usr" to ensure upgrade safety.
✅ Use SetValueExt for All Field Updates
Avoid direct assignment. Let events propagate correctly.
✅ Batch Database Operations
Never query the database inside loops. Use batch operations.
✅ Use Separate Graph Instances
Use PXGraph.CreateInstance() for related records to avoid conflicts.
✅ Prevent Recursion
Use flags or status checks to avoid infinite event loops.
✅ Test with Production Data
Always test customizations with production-like data volumes to catch performance issues.
Conclusion
📌 Series Wrap-up: This five-part series has covered the complete spectrum of data persistence in Acumatica events:
- Part 1: PXCache foundations and data modification methods
- Part 2: Persisting within specific events and advanced techniques
- Part 3: Validation, cross-table persistence, and performance optimization
- Part 4: Complex scenarios and production-ready patterns
- Part 5: Real-world case studies and troubleshooting
⚡ Final Thought: The key to successful Acumatica customization is understanding the framework's event model and persistence patterns. By following the principles and practices outlined in this series, you can build robust, maintainable customizations that stand the test of time.
🔍 Next Steps: Apply these patterns in your own projects. Start with simple customizations and gradually incorporate more complex techniques. The Acumatica Developer Network provides additional resources and community support.