Debugging Business Rules
Debugging Business Rules is harder than typical application code. Rules fire once per Data Unit — potentially thousands of times during a single calculation — and there is no step debugger.
The recommended approach is to collect trace messages in a nullable
StringBuilder, controlled by a debug flag, and output the full trace as a single error log entry through XFException. When debug is disabled, the pattern has zero performance overhead.The Debug Logging Pattern
Key elements of the pattern:
logdeclared at the class level — Accessible throughoutMainand any helper methods you add to the class.ENABLE_DEBUG/EnableDebugconstant — A compile-time flag. Set toTrueduring development,Falsefor production. Because it's a constant, the compiler can optimize away the dead code paths.If ENABLE_DEBUG Then log = New StringBuilder()— Only allocates theStringBuilderwhen debug is on. When off,logstaysNothing/null.log?.AppendLine(...)— The null-conditional operator. WhenlogisNothing/null, the entire call is skipped. Whenlogexists, the message is appended.XFExceptionin the Catch block — Wraps the accumulated log as thegeneralMsgparameter. If an error occurs, the full trace appears in the error log as a single entry alongside the exception details.
Why This Works — Classes and Functions
Class-Level Fields
When OneStream executes your rule, it instantiates your
MainClass and calls Main(). Fields declared outside Main — at the class level — are created when the class is instantiated and remain accessible to every method in the class.This matters because best practice is to keep
Main lean: it should check api.FunctionType and dispatch to dedicated methods for each function type. Declaring log at the class level means those helper methods can append to the same StringBuilder without passing it as a parameter.Nullable References
Declaring
log As StringBuilder = Nothing means the variable exists but holds no object. No StringBuilder is allocated in memory, no methods can run on it — it's just an empty reference. The debug constant decides whether to actually create the object with New StringBuilder().The Null-Conditional Operator (?.)
log?.AppendLine("msg") means: if log is not Nothing/null, call AppendLine; otherwise, skip the entire call. When debug is disabled, log stays Nothing/null — every log?.AppendLine() call becomes a no-op with zero performance overhead. No string allocations, no method calls, no cost.Controlling Where Execution Stops
Let It Fail Naturally
The most common approach: don't catch exceptions mid-rule. When an error occurs, it bubbles up to the outer
Try/Catch block, which passes the accumulated log through XFException. You see the full trace up to the point of failure.Throw Intentionally
Sometimes you need to inspect state at a specific point without waiting for an error. Throw a simple
Exception to halt execution — the outer Catch block will catch it and output the accumulated log through XFException automatically:This is useful when your rule completes successfully but produces unexpected results — throw at the point you want to inspect and the
Catch block handles the rest, packaging your accumulated log into the error output.Quick Messages with BRApi.ErrorLog
For simple one-off informational messages — not debug tracing —
BRApi.ErrorLog.LogMessage writes a single entry to the application error log:Related Content
- Getting Started with Finance Rules — Deep dive into Finance Business Rules — function types, Data Units, calculation techniques, and the Finance Rules API
- Getting Started with Workspace Assemblies — Organize your code into multi-file libraries with proper dependencies