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 on the same entity mark this as dirty when changed
Optional .DirtyByMethod(method) Marks this dirty after a method on the same entity runs
For 1 navigation hop .DirtyWithRelation(nav).DirtyBy(props) Marks dirty when a property on a related entity changes
For 2 navigation hops .DirtyWithRelation(hop1, hop2).DirtyBy(props) Marks dirty through a 2-level navigation chain
For 3 navigation hops .DirtyWithRelation(hop1, hop2, hop3).DirtyBy(props) Marks dirty through a 3-level navigation chain

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 a child collection

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

The reference property on the child side must be decorated with [OppositeSideCollection(..., CollectionLoadMode.LoadAll)].

[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);
    }
}

4. Pulling a value through a single navigation hop

An order's city comes from its customer. When Customer.City changes, or when the Customer reference itself is replaced, ShipToCity is marked dirty.

The navigation property must have an [OppositeSideProperty(...)] or [OppositeSideCollection(...)] attribute so the system can navigate back from the related entity.

[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
    [RegisterLogic]
    public void ComputeShipToCity()
    {
        salesOrder.Compute(o => o.ShipToCity)
            .From(o => o.Customer!.City)
            .DirtyWithRelation(o => o.Customer)
            .DirtyBy(c => c.City);
    }
}

5. Multi-level dirty: 2 navigation hops

When a computed value reaches through two related entities, pass both navigation selectors to DirtyWithRelation. The system registers dirty targets at every level of the chain automatically:

  • When PrimaryContact.FirstName changes → Description is dirtied
  • When Customer.PrimaryContact reference changes → Description is dirtied
  • When Order.Customer reference changes → Description is dirtied
[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
    [RegisterLogic]
    public void ComputeDescription()
    {
        salesOrder.Compute(o => o.Description)
            .If(o => o.Customer?.PrimaryContact != null)
            .From(o => $"{o.Customer!.PrimaryContact!.FirstName} {o.Customer!.PrimaryContact!.LastName}")
            .DirtyWithRelation(o => o.Customer, c => c.PrimaryContact)
            .DirtyBy(p => new { p.FirstName, p.LastName });
    }
}

Every navigation property in the chain must have an [OppositeSideProperty(...)] or [OppositeSideCollection(...)] attribute.

6. Multi-level dirty: 3 navigation hops

Three-level chains work the same way — pass all three selectors. The system handles dirty registration at all four levels (leaf, second intermediate, first intermediate, root).

[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
    [RegisterLogic]
    public void ComputeShipCity()
    {
        salesOrder.Compute(o => o.ShipCity)
            .If(o => o.Customer?.PrimaryContact?.MailingAddress != null)
            .From(o => o.Customer!.PrimaryContact!.MailingAddress!.City)
            .DirtyWithRelation(o => o.Customer, c => c.PrimaryContact, p => p.MailingAddress)
            .DirtyBy(a => a.City);
    }
}

7. 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);
            });
    }
}

DirtyWithRelation requirements

Every navigation property used in DirtyWithRelation must declare its opposite side so the system can navigate back from the related entity to the root:

Navigation type Required attribute on the navigation property
Reference (one-to-one) [OppositeSideProperty("PropName", "Label")]
Collection (one-to-many) [OppositeSideCollection("PropName", "Label", CollectionLoadMode.LoadAll)]

Performance note: CollectionLoadMode.LoadAll eagerly loads the full collection when navigating dirty relationships. Use it only when the collection is expected to be small.

Dirty from a method

DirtyByMethod marks the property dirty after a method runs — for when a method changes state the compute depends on but doesn't set directly.

counter.Compute(c => c.Result)
    .From(c => c.Counter * 10)
    .DirtyByMethod(c => c.Increment);

counter.Method(c => c.Increment).Do(c => c.Counter += 1);

After Increment runs, Result recomputes on the next read. Chain more .DirtyByMethod(...) calls to react to several methods.

Multiple subscribers on one property

Many [Logic] classes can compute the same property. A later subscriber reads the previous result with args.PriorSubscriberValue. Prior subscribers do not run unless that value is read.

// base value
invoice.Compute(d => d.UnitPrice).From(d => 30m).DirtyBy(d => d.Product);

// later subscriber adds to the prior value
invoice.Compute(d => d.UnitPrice)
    .From((d, args) => args.PriorSubscriberValue + 10m)
    .DirtyBy(d => d.Product);

PriorSubscriberValue is lazy (prior runs only when read), cached (read it twice, prior runs once), and returns default(T) when there is no prior subscriber. Order follows feature order, then [RegisterLogic] source order within a class.

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.