public

Value-based triggers

How do you ensure your business logic works? An advocate for testing might suggest that a successful test would mean it works, but this is only the case if it

Latest Post Value-based triggers by Magnus Gether Sørensen public

How do you ensure your business logic works? An advocate for testing might suggest that a successful test would mean it works, but this is only the case if it runs. Thus, we need to ensure that our business logic runs when we expect it to.

The traditional approach to business logic in plugins, workflows and cloud flows is to define a trigger and then the logic. In this post I will show how to flip that approach so you define your triggers based on your logic - in order to ensure that your logic works in all cases.

In this post, I will use simple examples to illustrate the method. Since they are simple, several edge-cases are missing in the actual logic - do not solve these requirements with the exact examples I provide.

Why do I need that?

Assume we have the following logic in a plugin that sets the name of the account to its address and postal code.


account.Name = $"{account.Address1_Line1} {account.Address1_PostalCode}";

If the plugin only triggers on changes to the address1 field but not the postal code, then the name would become incorrect if the postal code is updated. The same case can be shown in a cloud flow if the following expression is used to set the name.


concat(
    outputs('Account')?['body/address1_line1'],
    outputs('Account')?['body/address1_postalcode']
)

Triggers that are defined in a value-based way will never have this issue.

Defining a value-based trigger

A value based trigger is defined based on the fields that are used. In the previous example we assign address1 and the postal code to the name field. This means the two fields are used, and they should be in the trigger.

Let us assume the account naming should be conditional on some attribute. For example, if the account is inactive we set the name to something else.


account.Name = 
    account.StateCode == AccountState.Active
    ? $"{account.Address1_Line1} {account.Address1_PostalCode}"
    : "Inactive";

And for cloud flows


if(equals(outputs('Account')?['body/statecode'], 0),
    concat(
        outputs('Account')?['body/address1_line1'],
        outputs('Account')?['body/address1_postalcode']
    ),
    "Inactive"
)

In this case the statecode has an indirect impact on the value of the name attribute, thus the plugin or flow should trigger on all three attributes.

What if the value is based on a lookup?

This topic is quite intricate and leads to many moving parts. Therefore, I will only describe the solution in plugins and leave the Flow examples to another blog post.

There are two different cases for lookups

For the first case we could assume we add the name of the primary contact to the name of the account.


var primaryContact = Contact.Retrieve(orgAdminService, account.PrimaryContactId.Id);
account.Name = $"Account for {primaryContact.FullName}";

We depend on the lookup PrimaryContactId, so the account plugin should trigger on that. However, we also depend on a value for the related contact.

Therefore, we need two different triggers, one for the account for PrimaryContactId and one the contact for FullName. If we only defined a trigger based on the PrimaryContactId, we would never update the related account if the contact's fullname would change.

For the plugin, the account trigger should call a common function that would set the name.


public void SetAccountNameFromContact(Contact contact, Account account) {
    account.Name = $"Account for {contact.FullName}";
}

// code in trigger - triggering entity is account
var primaryContact = Contact.Retrieve(orgAdminService, account.PrimaryContactId.Id);
SetAccountNameFromContact(primaryContact, account);

And then have another plugin running on contact that would trigger on fullname


// code in trigger - triggering entity is contact
var relatedAccounts =
    context.AccountSet
    .Where(x => x.PrimaryContactId.Id == contact.Id)
    .Select(x => {
    	var toUpdate = new Account(x.Id);
    	SetAccountNameFromContact(contact,toUpdate);
        return toUpdate;
    })
    .ForEach(x => service.Update(x));

This ensures that whatever logic we have for naming an account is defined in a single place and is always called whenever the values used in the naming changes.

What about the other direction? If we name the account based on the number of related contacts?


public void SetAccountNameBasedOnContacts(Account accountId) {
    var relatedContactsCount =
        context.ContactSet
        .Where(x => x.AccountId.Id == account.Id)
        .Count();
        
    account.Name = $"Account for {relatedContactsCount} contacts";
}

In this scenario we do not need an update trigger for account, since the only attributes that change are on related records. The trigger for contact should trigger on change of AccountId, and call that logic above to set the name of the related account.


// code in trigger - triggering entity is contact
if (contact.AccountId == null) return;

var toUpdate = new Account(contact.AccountId.Id);
SetAccountNameBasedOnContacts(toUpdate);
service.Update(toUpdate);

Notice that naming of accounts is now defined solely based on plugins that trigger on contact. This is simply a result of how we defined our naming.

One last thing we need to ensure in this direction is the correctness of the previous value of AccountId. If we do nothing, then the account that we previously pointed to would never get a new name. Therefore, we also need to register a pre image on the contact to run the logic on the previous account. This results in this complete code.


public void SetAccountName(Contact contact) {
    if (contact?.AccountId == null) return;

    var toUpdate = new Account(contact.AccountId.Id);
    SetAccountNameBasedOnContacts(toUpdate);
    service.Update(toUpdate);
}

// code in trigger - triggering entity is contact, preimage is preImage
SetAccountName(contact);
SetAccountName(preImage);

Conclusion

By following the rules of value-based triggers we can ensure that our business logic is always triggered if a value we depend on is changed. If you decide not to trigger on an attribute that a value-based trigger would require you to, then you have taken an active decision that such a value is a snapshot.

Snapshot values are often needed in the real world. If you create invoices you would not want every historical invoice to change address if the parent account changed address. The learning from this blog post is that this should always be an active choice. Start with a complete value-based trigger - opt out of the attributes that should persist.

Magnus Gether Sørensen

Published a month ago