Default Event

Summary

The Default event sets a property's value when a triggering property changes and the target property is empty or has not been explicitly set by the user. It is the primary way to populate fields automatically based on related data — for example, copying a product's description onto a sales order line when the product is selected.

When does it fire?

Default uses the same mechanism as Compute. When the trigger property (specified with OnChange) changes, the target property is marked dirty. On the next read, the default expression runs — but only if the current value is empty/null.

Important: Do not use the Default event for static literal values. Use the [DefaultValue] attribute instead. Default events are for dynamic values based on other data.

Syntax

salesOrderDetail.Default(d => d.Description)
    .OnChange(d => d.Product)
    .From(d => d.Product?.SalesDescription ?? string.Empty);

Fluent API

Step Method Description
Required .OnChange(trigger) The property that, when changed, triggers the default
Required .From(expression) The expression that produces the default value
Alternative .FromIfNotNull(expression) Like From, but skips execution when the OnChange property is null

Scenarios

1. Populating a description from a selected product

When a user selects a product on a sales order line, the description should auto-fill from the product's sales description. If the user has already typed a description, it is not overwritten.

[Logic]
public class SalesOrderDetailBL(SalesOrderDetail.Logic salesOrderDetail)
{
    [RegisterLogic]
    public void DefaultDescription()
    {
        salesOrderDetail.Default(d => d.Description)
            .OnChange(d => d.Product)
            .From(d => d.Product?.SalesDescription ?? string.Empty);
    }
}

2. Defaulting a unit of measure from the product

When a product is selected, the default selling unit should be set as the UOM.

[Logic]
public class SalesOrderDetailBL(SalesOrderDetail.Logic salesOrderDetail)
{
    [RegisterLogic]
    public void DefaultUnit()
    {
        salesOrderDetail.Default(d => d.Unit)
            .OnChange(d => d.Product)
            .From(d => d.Product?.Uoms
                .FirstOrDefault(u => u.SellableOption == SellableOption.DefaultSellingUnit));
    }
}

3. Setting the price level from a selected customer

When the sell-to customer changes on a sales order, the price level defaults to that customer's price level.

[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
    [RegisterLogic]
    public void DefaultPriceLevel()
    {
        salesOrder.Default(o => o.PriceLevel)
            .OnChange(o => o.SellToCustomer)
            .From(o => o.SellToCustomer?.PriceLevel);
    }
}

4. Defaulting a shipping method from the customer's last order

When a customer is selected, look up their most recent order and carry forward the shipping method. If the shipping method was already set, keep the existing value.

[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
    [RegisterLogic]
    public void DefaultShippingMethod()
    {
        salesOrder.Default(o => o.ShippingMethod)
            .OnChange(o => o.SellToCustomer)
            .From((o, args) =>
            {
                var oldValue = o._State.ShippingMethod.ValidValue;
                if (oldValue != null)
                    return oldValue;

                if (o.SellToCustomer == null)
                    return null;

                var lastOrder = args.GetEntities<SalesOrder>()
                    .Where(s => s != o && s.SellToCustomer == o.SellToCustomer)
                    .OrderByDescending(s => s.OrderDate)
                    .FirstOrDefault();

                return lastOrder?.ShippingMethod;
            });
    }
}

5. Using FromIfNotNull to safely access the trigger property

FromIfNotNull is a variant of From that only runs the expression when the OnChange property is not null. This avoids null-checking inside the expression and prevents the default from firing when the trigger is cleared (e.g., a product is removed from a line).

[Logic]
public class SalesOrderDetailBL(SalesOrderDetail.Logic salesOrderDetail)
{
    [RegisterLogic]
    public void DefaultLineOrder()
    {
        salesOrderDetail.Default(d => d.LineOrder)
            .OnChange(d => d.SalesOrder)
            .FromIfNotNull(d =>
            {
                var lastDetail = d.SalesOrder!.Details
                    .Where(detail => detail != d)
                    .OrderBy(detail => detail.LineOrder)
                    .LastOrDefault();

                return lastDetail == null ? 1 : lastDetail.LineOrder + 1;
            });
    }
}

Compare this with the equivalent From version, which must handle the null case manually:

// With From — must null-check manually
salesOrderDetail.Default(d => d.LineOrder)
    .OnChange(d => d.SalesOrder)
    .From(d => d.SalesOrder == null ? 0
        : (d.SalesOrder.Details.Where(detail => detail != d)
            .OrderBy(detail => detail.LineOrder).LastOrDefault()?.LineOrder ?? 0) + 1);

// With FromIfNotNull — expression only runs when SalesOrder is set
salesOrderDetail.Default(d => d.LineOrder)
    .OnChange(d => d.SalesOrder)
    .FromIfNotNull(d => ...);

Default vs. Compute

Default Compute
Runs when Target is empty/null and trigger changes Target is dirty and read
Use for Initial/suggested values from related entities Derived/calculated values
Trigger Specific property change (OnChange) Any dirty dependency (DirtyBy)

Notes

  • For static default values (e.g., Quantity = 1), use the [DefaultValue(1)] attribute on the property in the entity model instead.