How to get a reliable xRec

11 May

This tweet reminded me of a small but beautiful trick to get a reliable xRec in all the Modify trigger events.

Which resulted in this new thread:

The problem

When events were introduced in C/AL code, one of the most discussed was the OnAfterModify event. Back in 2016, Vjeko wrote an article about it: https://vjeko.com/2016/05/17/onafter-table-event-subscriber-patterns-and-antipatterns/.

There are actually two problems that play a role here. The first one is that the value of xRec can’t be trusted. The variable xRec is supposed to have the original value of the variable Rec when a record is modified. However, this is only the case when the user modified the record from the UI. When a record is modified by code with Rec.Modify(), then xRec has the current value of Rec instead of the original value.

So, when you have code like this (silly example, but it explains it pretty well), then it doesn’t work for modifies done by code:

    [EventSubscriber(ObjectType::Table, Database::Customer, OnAfterModifyEvent, '', false, false)]
    local procedure OnAfterModifyCustomer(var Rec: Record Customer; var xRec: Record Customer; RunTrigger: Boolean)
    begin
        if (Rec.Name <> xRec.Name) then
            Message('Customer name changed from %1 to %2', xRec.Name, Rec.Name);
    end;

The second problem is that the event OnAfterModifyEvent occurs after the database write. As a result, it doesn’t help to read the record again from the database, because you will then still get the updated values. I wrote an article about the order of events back in 2018: https://www.kauffmann.nl/2018/03/24/table-trigger-events-in-dynamics-365-business-central/.

The tableextension does have an OnModify trigger that may help in some situations, but not all. Consider this code:

tableextension 50100 CustomerExt extends Customer
{    
    trigger OnModify()
    var
        OldRec: Record Customer;
    begin
        SelectLatestVersion();
        OldRec.Get(Rec."No.");
        if (Rec.Name <> OldRec.Name) then
            Message('Customer name changed from %1 to %2 (from tableextension)', OldRec.Name, Rec.Name);
    end;
}

First of all, this event is only triggered when you do Modify(true). And my tests also show that it doesn’t work when the record is changed in the web client. In that case, OldRec contains the new value! I didn’t expect that, but even adding SelectLatestVersion didn’t help. I’d be glad if someone can confirm that this works on older versions, but maybe I’m totally wrong here.

Anyway, as you can see working with xRec is dangerous. So now the question is how to solve this?

The solution

I’ve seen different solutions, most of them were something like the code below. Storing the OldRec in a global variable in a single instance codeunit.

codeunit 50100 CustomerEvents
{
    SingleInstance = true;
    var
        OldRec: Record Customer;
    [EventSubscriber(ObjectType::Table, Database::Customer, OnBeforeModifyEvent, '', false, false)]
    local procedure OnBeforeModifyCustomer(var Rec: Record Customer; var xRec: Record Customer; RunTrigger: Boolean)
    begin
        SelectLatestVersion();
        OldRec.Get(Rec."No.")
    end;
    [EventSubscriber(ObjectType::Table, Database::Customer, OnAfterModifyEvent, '', false, false)]
    local procedure OnAfterModifyCustomer(var Rec: Record Customer; var xRec: Record Customer; RunTrigger: Boolean)
    begin
        if (Rec.Name <> OldRec.Name) then
            Message('Customer name changed from %1 to %2 (from event subscriber)', OldRec.Name, Rec.Name);
    end;
}

This is a working solution for this codeunit. But it is only working inside this codeunit. Other event subscribers or the triggers in the tableextension do not benefit from it.

So I would like to introduce another solution that works quite well. Credits for this solution goes to a student of one of my AL development classes who came up with it. He said: ‘Look at the xRec parameter in the OnBeforeModifyEvent. It is by var, does that mean you can change it?’. And that was spot on! The code below refreshes xRec from the database and that makes it useful for all triggers that come after it. Only other OnBeforeModifyEvent subscribers can’t rely on it because you don’t know in which order the same events are executed.

codeunit 50100 CustomerEvents
{
    [EventSubscriber(ObjectType::Table, Database::Customer, OnBeforeModifyEvent, '', false, false)]
    local procedure OnBeforeModifyCustomer(var Rec: Record Customer; var xRec: Record Customer; RunTrigger: Boolean)
    begin
        SelectLatestVersion();
        xRec.Get(xRec."No.");
    end;
    [EventSubscriber(ObjectType::Table, Database::Customer, OnAfterModifyEvent, '', false, false)]
    local procedure OnAfterModifyCustomer(var Rec: Record Customer; var xRec: Record Customer; RunTrigger: Boolean)
    begin
        if (Rec.Name <> xRec.Name) then
            Message('Customer name changed from %1 to %2 (from OnAfterModifyEvent subscriber)', xRec.Name, Rec.Name);
    end;
}

To be honest, I still consider this a dirty workaround for something that should be solved by the platform. In my opinion, xRec should be reliable in all situations.

And I also still hate the design choice that something that is called OnAfterModifyEvent occurs not immediately after the event, but after the database write. With the excuse that modify here means: ‘writing to the database’. We, as AL developers, think in terms of events, and then OnAfterModifyEvent should occur immediately after the event, and not at the end of a chain of other events. Anyway… we are used to the sometimes not so logic design in AL code. And maybe I’m the only one with this opinion, what do I know…

One thought on “How to get a reliable xRec

  1. I had a scenario which some code overwrote data on items. I couldn’t find it in code so we used this together with OnAfterModify to see where the change was made in code.
    Hower we had to change
    xRec.Get(xRec.”No.”);
    to
    if xRec.”No” ” then xRec.Get(xRec.”No.”);
    Otherwise the item template functionality stopped working because when xRec.”No.” didn’t exist it turned into an error.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.