Subscriber Codeunits

Created by waldo, Described by waldo

Description

In general, subscribers have to be put in codeunits. There are a few performance considerations that you should keep in the back of your minds, when designing such a codeunit.

  • Keep the codeunit as small as possible
  • Work with a single instance codeunit
  • only subscribe when necessary
  • Avoid generic OnInsert/OnModify/OnDelete

Let’s discuss all points

Keep the codeunit as small as possible

Every time a subscriber gets called, a new instance of the codeunit is being loaded in memory, which takes memory and processing power. The smaller the codeunit, the less memory, and the faster it is.

Therefore, it’s suggested to split the subscribers by functionality and avoid putting business logic in the actual codeunit. Tip: put all business logic in an “Method Codeunit”.

Examples:

  • if you app does things on Sales and Purchase, create a Sales-subs codeunit, and a Purchase-subs.
  • if you have multiple functionalities in your app (let’s call’m modules), create a subs-codeunit per module, and only add the subscribers in there that are necessary for that module.

Bad code

codeunit 2037325 "Setup Subs"
{
    SingleInstance = true;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Manual Setup", 'OnRegisterManualSetup', '', false, false)]
    local procedure OnRegisterManualSetup(sender: Codeunit "Manual Setup")
    var
        AppId: ModuleInfo;
        NameListLbl: Label 'Linked Texts Framework - List', Locked = true;
        DescriptionListLbl: Label 'Edit linked texts', Locked = true;
        KeyWordListLbl: Label 'LT,Distri,Technical,Functional,Reports', Locked = true;
        NameReportLbl: Label 'Linked Texts Framework - Reports', Locked = true;
        DescriptionReportLbl: Label 'View linked texts reports', Locked = true;
        KeyWordReportLbl: Label 'LT,Distri,Technical,Functional,Reports', Locked = true;
    begin
        navapp.GetCurrentModuleInfo(AppId);
        Sender.Insert(NameListLbl, DescriptionListLbl, KeyWordListLbl, page::"LTE Linked Text List", AppId.Id(), "Manual Setup Category"::General);
        Sender.Insert(NameReportLbl, DescriptionReportLbl, KeyWordReportLbl, page::"LTE Linked Texts Reports", AppId.Id(), "Manual Setup Category"::General);
    end;
        
    [EventSubscriber(ObjectType::Codeunit, codeunit::"Manual Setup", 'OnRegisterManualSetup', '', false, false)]
    local procedure OnRegisterManualSetup(sender: Codeunit "Manual Setup")
    var
        AppId: ModuleInfo;
        NameLayoutLbl: Label 'Report Helper - Layout', Locked = true;
        DescriptionLayoutLbl: Label 'Set up or update report layout list', Locked = true;
        KeyWordLayoutLbl: Label 'RH,Distri,Technical,Functional,Reports,Layout', Locked = true;
        NameCaptionsLbl: Label 'Report Helper - Captions', Locked = true;
        DescriptionCaptionsLbl: Label 'Set up or update captions list', Locked = true;
        KeyWordCaptionsLbl: Label 'RH,Distri,Technical,Functional,Reports,Captions', Locked = true;
        NameFunctionsLbl: Label 'Report Helper - Functions', Locked = true;
        DescriptionFunctionsLbl: Label 'Set up or disable functions', Locked = true;
        KeyWordFunctionsLbl: Label 'RH,Distri,Technical,Functional,Reports,Functions', Locked = true;
        NameDFCLbl: Label 'Report Helper - Default Footer', Locked = true;
        DescriptionDFCLbl: Label 'Set up or update default footer', Locked = true;
        KeyWordDFCLbl: Label 'RH,Distri,Technical,Functional,Reports,Default,Footer', Locked = true;
    begin
        navapp.GetCurrentModuleInfo(AppId);
        Sender.Insert(NameLayoutLbl, DescriptionLayoutLbl, KeyWordLayoutLbl, page::"RHE Report Layout List", AppId.Id(), "Manual Setup Category"::General);
        Sender.Insert(NameCaptionsLbl, DescriptionCaptionsLbl, KeyWordCaptionsLbl, page::"RHE Captions", AppId.Id(), "Manual Setup Category"::General);
        Sender.Insert(NameFunctionsLbl, DescriptionFunctionsLbl, KeyWordFunctionsLbl, page::"RHE Functions", AppId.Id(), "Manual Setup Category"::General);
        Sender.Insert(NameDFCLbl, DescriptionDFCLbl, KeyWordDFCLbl, page::"RHE Default Footer Card", AppId.Id(), "Manual Setup Category"::General);
    end;
}

Good code

Split into 2 codeunits, and move the business logic out.

codeunit 2037325 "LTE Setup Subs"
{
    SingleInstance = true;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Manual Setup", 'OnRegisterManualSetup', '', false, false)]
    local procedure OnRegisterManualSetup(sender: Codeunit "Manual Setup")
    var
        RegisterLTEManualSetup: codeunit "Register LTE Manual Setup";
    begin 
        RegisterLTEManualSetup.RegisterLTEManualSetup();
    end;
}

codeunit 2037324 "RHE Setup Subs"
{
    SingleInstance = true;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Manual Setup", 'OnRegisterManualSetup', '', false, false)]
    local procedure OnRegisterManualSetup(sender: Codeunit "Manual Setup")
    var
        RegisterRHEManualSetup: codeunit "Register RHE Manual Setup";
    begin 
        RegisterRHEManualSetup.RegisterRHEManualSetup();
    end;
}

Work with a single instance codeunit

To avoid the extra “loading of the content” while a subscriber is being executed, use Single Instance codeunit for subscribers. Do take into account, of course, that it would share the state across the entire session.

Bad code

codeunit 2037324 "RHE Setup Subs"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Manual Setup", 'OnRegisterManualSetup', '', false, false)]
    local procedure OnRegisterManualSetup(sender: Codeunit "Manual Setup")
    var
        RegisterRHEManualSetup: codeunit "Register RHE Manual Setup";
    begin 
        RegisterRHEManualSetup.RegisterRHEManualSetup();
    end;
}

Good code

codeunit 2037324 "RHE Setup Subs"
{
    SingleInstance = true;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Manual Setup", 'OnRegisterManualSetup', '', false, false)]
    local procedure OnRegisterManualSetup(sender: Codeunit "Manual Setup")
    var
        RegisterRHEManualSetup: codeunit "Register RHE Manual Setup";
    begin 
        RegisterRHEManualSetup.RegisterRHEManualSetup();
    end;
}

only subscribe when necessary

If possible, only execute the subscriber when really necessary by using Manual Binding.

Bad code

    //subscriber - code should actually only run when Color=Red.
    [EventSubscriber(ObjectType::Table, Database::"Just Some Table WLD", 'OnAfterValidateEvent', 'Message 2', false, false)]
    local procedure JustDoSomthing(var Rec: Record "Just Some Table WLD"; var xRec: Record "Just Some Table WLD")
    begin
        if Rec.color <> 'RED' then
            exit; //only execute when necessary

        ...
    end;
    
    //business logic
    if JustSomeTable.FindSet() then
        repeat
            JustSomeTable.Validate("Message 2", format(Random(1000))); 
        until JustSomeTable.Next() < 1;

Good code

    if JustSomeTable.FindSet() then
        repeat
            if JustSomeTable.Color = 'RED' then 
                BindSubscription(DemoSubs);

            JustSomeTable.Validate("Message 2", format(Random(1000)));

            if JustSomeTable.Color = 'RED' then 
                UnbindSubscription(DemoSubs);
        until JustSomeTable.Next() < 1;

Avoid OnInsert/OnModify/OnDelete

The reason for this is, that it breaks the batch-calls:

  • Any “OnInsert” subscriber breaks the bulk inserts, simply because it needs to perform an operation after every record that was inserted
  • Any “OnModify” subscriber slows down the “ModifyAll”, simply because it needs to perform an operation after every record that was modified. I fact: 1 SQL call is turned into a loop of SQL calls.
  • Any “OnDelete” subscriber slows down the “DeleteAll”, simply because it needs to perform an operation after every record that was deleted. I fact: 1 SQL call is turned into a loop of SQL calls.

Avoid subscribers to these events.

References

The Generic Method Pattern

Last modified February 24, 2022: Removed Discussion Links. Using new Theme solution. (fb17dc96) by Henrik Helgesen