Compute Event

Summary

The Compute event defines how a property value is calculated from other properties or data. Computed properties are lazily evaluated — they only recalculate when they are dirty (marked as needing recomputation). A property becomes dirty when one of its declared dependencies changes.

When does it fire?

A compute event fires when a dirty property is read (via its getter). It does not fire on every property change — only when the computed value is actually needed.

Get property → Is dirty? → Yes → Run Compute → Return new value
                         → No  → Return cached value

Syntax

salesOrderDetail.Compute(d => d.TotalPrice)
    .From(d => d.Quantity * d.UnitPrice)
    .DirtyBy(d => new { d.Quantity, d.UnitPrice });

Fluent API

Step Method Description
Optional .If(condition) Only compute when the condition is true
Required .From(expression) The expression that produces the computed value
Recommended .DirtyBy(properties) Which properties, when changed, mark this property as dirty
For collections .DirtyWithRelation(collection) Marks dirty when items in a child collection change

Scenarios

1. Calculating a line total from quantity and price

A sales order detail's total price is the product of quantity and unit price. Whenever either value changes, the total must be recalculated.

[Logic]
public class SalesOrderDetailBL(SalesOrderDetail.Logic salesOrderDetail)
{
    [RegisterLogic]
    public void ComputeTotalPrice()
    {
        salesOrderDetail.Compute(d => d.TotalPrice)
            .From(d => d.Quantity * d.UnitPrice)
            .DirtyBy(d => new { d.Quantity, d.UnitPrice });
    }
}

2. Deriving a display name from multiple fields

A contact's full name is built from first and last name. It recomputes whenever either name part changes.

[Logic]
public class ContactBL(Contact.Logic contact)
{
    [RegisterLogic]
    public void ComputeFullName()
    {
        contact.Compute(c => c.FullName)
            .From(c => $"{c.FirstName} {c.LastName}".Trim())
            .DirtyBy(c => new { c.FirstName, c.LastName });
    }
}

3. Computing a parent total from child collection

A sales order's subtotal is the sum of its detail line totals. The DirtyWithRelation method connects the parent to the child collection so that adding, removing, or changing a detail line marks the subtotal as dirty.

[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
    [RegisterLogic]
    public void ComputeSubtotal()
    {
        salesOrder.Compute(o => o.Subtotal)
            .From(o => o.Details.Sum(d => d.TotalPrice))
            .DirtyWithRelation(o => o.Details)
            .DirtyBy(d => d.TotalPrice);
    }

    [RegisterLogic]
    public void ComputeTotal()
    {
        salesOrder.Compute(o => o.Total)
            .From(o => o.Subtotal + o.ShippingAmount)
            .DirtyBy(o => new { o.Subtotal, o.ShippingAmount });
    }
}

4. Computing a sales order total from the detail collection

A sales order's total combines the subtotal (sum of detail lines) with shipping. The subtotal uses DirtyWithRelation to track the child Details collection — whenever a detail line is added, removed, or its TotalPrice changes, the subtotal is marked dirty. The total then depends on the subtotal and shipping amount using regular DirtyBy.

[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
    [RegisterLogic]
    public void Totals()
    {
        salesOrder.Subtotal.Compute()
            .From(doc => doc.Details.Sum(detail => detail.TotalPrice))
            .DirtyWithRelation(o => o.Details)
            .DirtyBy(d => [d.TotalPrice]);

        salesOrder.Total.Compute()
            .From(doc => doc.Subtotal + doc.ShippingAmount)
            .DirtyBy(doc => [doc.Subtotal, doc.ShippingAmount]);
    }
}

5. Compute with service injection

When the compute expression needs external data or services, use the overload that provides args.

[Logic]
public class CustomerBL(Customer.Logic customer)
{
    [RegisterLogic]
    public void ComputeOpenOrderCount()
    {
        customer.Compute(c => c.OpenOrderCount)
            .From((c, args) =>
            {
                return args.GetEntities<SalesOrder>()
                    .Count(o => o.SellToCustomer == c
                             && o.Status == SalesOrderStatus.Open);
            });
    }
}

Notes

  • If DirtyBy is omitted, the property is only dirty on load (it computes once per entity load).
  • A Compute and Changed subscriber pair on the same property will not cause cycles. The Changed subscriber does not fire when the value is set by the Compute subscriber.
  • Compute conditions (.If()) must be fast — they are evaluated on every read of a dirty property.