Debugging Events
//TODO: This could be simplified.
When debugging business logic, it can be difficult to understand why a property changed, what triggered the change, and which events ran in response. The DebuggerPropertyNames feature lets you set a breakpoint on any property change at runtime — without modifying code or rebuilding.
This feature is only available in Debug builds. It is completely removed in Release builds with zero performance impact.
Quick Start
- Run your app or test in Debug mode
- Hit any breakpoint (or use
Debug.Break()) - In the Immediate Window, add the property you want to track:
SalesOrder.DebugPropertyNames.Add("Subtotal")
- Continue execution. The debugger will break the next time
Subtotalis set or dirtied on anySalesOrderinstance.
How It Works
Every entity class generated with [ApiEntity] includes a static HashSet<string> in Debug builds:
public static HashSet<string> DebugPropertyNames = [];
When a property is set — either directly or by being dirtied through a dependency — the generated code checks whether the property name is in this set. If it is, the debugger breaks and exposes diagnostic variables.
What Triggers a Break
The debugger breaks in two scenarios:
| Scenario | isDirty |
Meaning |
|---|---|---|
| Direct set | false |
Code explicitly assigned a value to this property |
| Dependency dirtied | true |
Another property changed and this property was marked dirty as a dependent |
This distinction tells you whether you're looking at the cause or the effect of a change.
Diagnostic Variables
When the debugger breaks, these local variables are available in the Watch or Locals window:
| Variable | Type | Description |
|---|---|---|
getValue |
Func<TValue> |
Call getValue() in the Immediate window to see the value being set. Intentionally lazy to avoid side effects. |
changedBy |
string? |
The event or logic method that set this property |
dirtiedBy |
string? |
The property that caused this property to be dirtied (when isDirty is true) |
dirtyFlag |
bool |
false = direct set, true = dirtied by dependency |
eventsTree |
string |
Full tree of all events that have fired for this entity in the current context |
eventPath |
string |
The chain of events leading to this specific change |
context |
EventContext |
The full event context for advanced inspection |
Reading eventsTree
The events tree shows a hierarchy of all events that fired, indented by depth:
Compute:SalesOrderDetail.TotalPrice
Compute:SalesOrder.Subtotal
Compute:SalesOrder.Total
This tells you: setting TotalPrice triggered a recompute of Subtotal, which triggered a recompute of Total.
Reading eventPath
The event path is a flat chain showing how the current breakpoint was reached:
Compute:SalesOrderDetail.TotalPrice -> Compute:SalesOrder.Subtotal
Common Scenarios
"Why did this value change?"
Track the property and inspect changedBy when it breaks:
SalesOrderDetail.DebugPropertyNames.Add("UnitPrice")
When it breaks, check changedBy — it will show the compute event or logic method that set the value (e.g., "PricingBL.ComputeUnitPrice").
"What triggered this recomputation?"
Track a computed property and check dirtiedBy:
SalesOrder.DebugPropertyNames.Add("Total")
When it breaks with dirtyFlag = true, dirtiedBy tells you which property change caused Total to be marked dirty (e.g., "Subtotal" or "ShippingAmount").
"What's the full chain of events?"
Inspect eventsTree to see every event that has fired on this entity during the current operation. This is especially useful for understanding cascading compute chains.
Track Multiple Properties
Add as many properties as you need:
SalesOrder.DebugPropertyNames.Add("Subtotal")
SalesOrder.DebugPropertyNames.Add("Total")
SalesOrder.DebugPropertyNames.Add("ShippingAmount")
Stop Tracking
Remove a specific property or clear all:
// Stop tracking one property
SalesOrder.DebugPropertyNames.Remove("Total")
// Stop tracking all properties on this entity
SalesOrder.DebugPropertyNames.Clear()
Track on Different Entities
Each entity class has its own DebugPropertyNames — they are independent:
SalesOrder.DebugPropertyNames.Add("Total")
SalesOrderDetail.DebugPropertyNames.Add("TotalPrice")
Customer.DebugPropertyNames.Add("FullName")
Important Notes
- Debug builds only — the feature is compiled out in Release. Use
dotnet build -c Debugor run from Visual Studio in Debug configuration. - Static collection —
DebugPropertyNamesis shared across all instances of an entity class. Adding"Total"breaks on everySalesOrderinstance, not just one. - Lazy value —
getValueis aFunc<TValue>. CallgetValue()in the Immediate window only when you need the value. This avoids triggering additional side effects during debugging. - Property names are case-sensitive — use the exact C# property name (e.g.,
"Subtotal", not"subtotal"). - No rebuild needed — add or remove property names at runtime through the Immediate window. No code changes or restart required.