Inverting Dependencies with Interfaces in Business Central Application Language

How to effectively use interfaces in AL and manage your Business Central application source code better.

Inverting Dependencies with Interfaces in Business Central Application Language
Photo by Justin Wilkens / Unsplash

Although interfaces were introduced in Business Central 2020 release wave 1, it can still be a fairly new concept for Business Central (BC) developers who have not programmed in other language such as C#, TypeScript, or Java. Interfaces are not new in the software engineering world, and it’s never too late to catch up with the rest of the industry on the good practices of writing and maintaining source code, and in particular, programming against an interface, not an implementation. Let’s have a look at an evolution of a simplistic piece of software to demonstrate the benefits of using interfaces.

Disclaimer: the names of companies and providers used in this blog post except Microsoft are fictitious. Any similarity to the names is entirely coincidental.

Setting the stage

To find a theme for our simple solution written in Application Language (AL), we’ll refer to the trendy tool the companies are implementing these days - AI. Let’s imagine, our business leaders asked us to create a Business Central application for helping with some users' questions, and we’re the developers from the past without interfaces in AL. We (at least I) have no idea how to build proper prompts to real AI services, and although it won’t often happen in real life, our manager agreed with my suggestion that calling real services of AI providers is out of scope of our solution. We decided to hardcode the responses from the services. Although everyone feels like this implementation may not bring any value to the business and could be a complete waste of time, the product should be enough to see the power of adding interfaces to your software development tool set. That’s the real purpose of the project - learning something new!

We’ll start with a simple AL application containing a page, an element to enter a question from a user, and an action button to get a response from the fake AI services. The page code below will be enough to get first development results.

page 50100 "ASH Fake AI Service"
{
    ApplicationArea = Basic, Suite;
    Caption = 'Fake AI Service';
    DeleteAllowed = false;
    InsertAllowed = false;
    PageType = Card;
    UsageCategory = Tasks;

    layout
    {
        area(content)
        {
            field(Question; Question)
            {
                ApplicationArea = All;
                Caption = 'Question';
                MultiLine = true;
                ToolTip = 'The question to ask the AI service.';
            }
        }
    }

    actions
    {
        area(Processing)
        {
            action("Get AI Answer")
            {
                Caption = 'Get AI Answer';
                ApplicationArea = All;
                Image = Answers;
                ToolTip = 'Get the answer from the AI service.';
                Promoted = true;
                PromotedCategory = Process;
                PromotedIsBig = true;

                trigger OnAction()
                var
                    FakeAiServiceMgt: Codeunit "ASH Fake AI Service Mgt.";
                begin
                    Message(FakeAiServiceMgt.GetAnswer(Question));
                end;
            }
        }
    }

    var
        Question: Text;

}
The beginning of page 50100 "ASH Fake AI Service"

And the codeunit provides an answer already, which is a good start.

codeunit 50100 "ASH Fake AI Service Mgt."
{
    procedure GetAnswer(Question: Text): Text
    begin
        if Question = '' then
            Error('Please enter your question.');

        exit('answer');
    end;
}
The first version of codeunit 50100 "ASH Fake AI Service Mgt."

Our current solution is action should look like in the figure below.

Fake AI Service page with a question and an answer
Fake AI Service page with a question and an answer

Let’s call our first two fake AI service providers Bingle and Alfasoft. The system logic to call their services is a bit different and can change later, so we’ll put them in dedicated codeunits and will call them from our main Fake AI Service Mgt. codeunit. To specify the provider, we’ll create an extensible Enum object and add a new field with it to the page just above the Question element.

enum 50100 "ASH Fake AI Service Providers"
{
    Extensible = true;

    value(50100; Bingle)
    {
    }
    value(50101; Alfasoft)
    {
    }
}
First version of enum 50100 "ASH Fake AI Service Providers"
page 50100 "ASH Fake AI Service"
{
    ApplicationArea = Basic, Suite;
    Caption = 'Fake AI Service';
    DeleteAllowed = false;
    InsertAllowed = false;
    PageType = Card;
    UsageCategory = Tasks;

    layout
    {
        area(content)
        {
            field(Provider; Provider)
            {
                ApplicationArea = All;
                Caption = 'Provider';
                ToolTip = 'The AI service provider to use.';
            }

            field(Question; Question)
            {
                ApplicationArea = All;
                Caption = 'Question';
                MultiLine = true;
                ToolTip = 'The question to ask the AI service.';
            }
        }
    }

    actions
    {
        area(Processing)
        {
            action("Get AI Answer")
            {
                Caption = 'Get AI Answer';
                ApplicationArea = All;
                Image = Answers;
                ToolTip = 'Get the answer from the AI service.';
                Promoted = true;
                PromotedCategory = Process;
                PromotedIsBig = true;

                trigger OnAction()
                var
                    FakeAiServiceMgt: Codeunit "ASH Fake AI Service Mgt.";
                begin
                    Message(FakeAiServiceMgt.GetAnswer(Provider, Question));
                end;
            }
        }
    }

    var
        Question: Text;
        Provider: Enum "ASH Fake AI Service Providers";
}
Providers element added to page 50100 "ASH Fake AI Service"

The main codeunit GetAnswer procedure now needs the Provider parameter so that depending on the enum value that we select on the page, we are able to get answers from the service providers.

codeunit 50100 "ASH Fake AI Service Mgt."
{
    procedure GetAnswer(Provider: Enum "ASH Fake AI Service Providers"; Question: Text) Answer: Text
    var
        AlfasoftAiServiceMgt: Codeunit "ASH Alfasoft AI Service Mgt.";
        BingleAiServiceMgt: Codeunit "ASH Bingle AI Service Mgt.";
    begin
        if Question = '' then
            Error('Please enter your question.');

        case Provider of
            Provider::Bingle:
                Answer := BingleAiServiceMgt.GetAnswer(Question);
            Provider::Alfasoft:
                Answer := AlfasoftAiServiceMgt.GetAnswer(Question);
        end;
    end;
}
Codeunit 50100 "ASH Fake AI Service Mgt." logic for different providers

The dedicated to service provider logic codeunits are listed below.

codeunit 50101 "ASH Alfasoft AI Service Mgt."
{
    procedure GetAnswer(Question: Text) Answer: Text
    begin
        Answer := 'Alfasoft says: hmm, interesting question';
    end;
}


codeunit 50102 "ASH Bingle AI Service Mgt."
{
    procedure GetAnswer(Question: Text) Answer: Text
    begin
        Answer := 'Bingle says: you know it better';
    end;
}
Answer logic for Bingle and Alfasoft providers
Service answers are now based on the specified provider
Service answers are now based on the specified provider

Notice how we must update the main Fake AI Service Mgt. codeunit every time we need to add a service provider. All the services implementations are explicitly referenced in the area where we defined our variables. This is called a tightly coupled code - changing in one codeunit often must be reflected in the other codeunit as well.

Now imagine that our service providers list keeps growing as well as related features and the main codeunit at some point becomes hard to read. The cases when one service was broken after making changes for another have started to pop up and eventually no one wants to touch that code anymore. The time it takes to ensure that nothing is broken starts to delay delivery of new application features.

To add fuel to the flame, a couple of our customers want to add their custom AI breakthrough solutions to the list as well but it sounds like we have no other choice apart from letting them customize our application for them and include their codeunits into our codebase. We don’t believe it will be a good approach for maintaining and developing our solution further. Time to seriously think of a new way of organizing our code. Luckily for us, Microsoft introduced interfaces to Business Central Application Language, and we’ve decided to explore this new platform feature.

Adding an interface

From the Microsoft Learn article about interfaces in AL we understood that an interface is a syntactical contract which needs to be implemented. Let’s have a look at our service providers code that we wrote for getting the answers. If we look at the service provider management codeunits as implementations with specific to provider details, what is the common function that a service provider codeunit has? That’s right, the GetAnswer function. Our interface for fake AI service providers will look as follows.

interface "ASH AI Service Provider"
{
    procedure GetAnswer(Question: Text) Answer: Text
}
Interface "ASH AI Service Provider"

The important part here is that the method signature - the procedure name, the parameters, and the returned value - must be exactly the same as we would expect it in our implementations. The visibility of a procedure will always be public there. We need interfaces only to define how our system will be communicating with an implementation. We won’t be able to see private methods anyway.

Our providers services codeunits have procedures with the method signatures from the interface and therefore we can say that they implement it. We can explicitly say so in the program code by adding the implements keyword.

codeunit 50101 "ASH Alfasoft AI Service Mgt." implements "ASH AI Service Provider"
{
    procedure GetAnswer(Question: Text) Answer: Text
    begin
        Answer := 'Alfasoft says: hmm, interesting question';
    end;
}

codeunit 50102 "ASH Bingle AI Service Mgt." implements "ASH AI Service Provider"
{
    procedure GetAnswer(Question: Text) Answer: Text
    begin
        Answer := 'Bingle says: you know it better';
    end;
}
Alfasof and Bingle services codeunits implement the interface

After rewriting our main codeunit, the code looks as below.

codeunit 50100 "ASH Fake AI Service Mgt."
{
    procedure GetAnswer(Provider: Enum "ASH Fake AI Service Providers"; Question: Text) Answer: Text
    var
        AlfasoftAiServiceMgt: Codeunit "ASH Alfasoft AI Service Mgt.";
        BingleAiServiceMgt: Codeunit "ASH Bingle AI Service Mgt.";
        AIServiceProvider: Interface "ASH AI Service Provider";
    begin
        if Question = '' then
            Error('Please enter your question.');

        case Provider of
            Provider::Bingle:
                AIServiceProvider := BingleAiServiceMgt;
            Provider::Alfasoft:
                AIServiceProvider := AlfasoftAiServiceMgt;
        end;

        Answer := AIServiceProvider.GetAnswer(Question);
    end;
}
Our main codeunit contains assignments of our implementations to the interface variable depending on the Provider

We have defined a new variable of type Interface and assigned an implementation codeunit to it depending on the Provider parameter. Once we have an implementation assigned, we can call the GetAnswer method. Notice that we do it using the interface variable which at the moment of calling the method has a reference to the relevant instance of the implementation codeunit. Can we use this for extending our solution for the customers? Exposing an event sounds like a good option, so let’s do it!

codeunit 50100 "ASH Fake AI Service Mgt."
{
    procedure GetAnswer(Provider: Enum "ASH Fake AI Service Providers"; Question: Text) Answer: Text
    var
        AlfasoftAiServiceMgt: Codeunit "ASH Alfasoft AI Service Mgt.";
        BingleAiServiceMgt: Codeunit "ASH Bingle AI Service Mgt.";
        AIServiceProvider: Interface "ASH AI Service Provider";
    begin
        if Question = '' then
            Error('Please enter your question.');

        case Provider of
            Provider::Bingle:
                AIServiceProvider := BingleAiServiceMgt;
            Provider::Alfasoft:
                AIServiceProvider := AlfasoftAiServiceMgt;
            else
                OnBeforeGetAnswer(Provider, AIServiceProvider);
        end;
        
        Answer := AIServiceProvider.GetAnswer(Question);
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeGetAnswer(Provider: Enum "ASH Fake AI Service Providers"; var AIServiceProvider: Interface "ASH AI Service Provider")
    begin
    end;

}
OnBeforeGetAnswer integration event added to the main codeunit

All our customers have to do now is to extend the enum 50100 "ASH Fake AI Service Providers" and subscribe to the new handy event where they can assign their implementations to the AIServiceProvider variable.

Creating an extension to the main app

To demonstrate that, let’s create a separate app for the new fake AI service provider from the company called Apricot, and name the app Custom AI Provider. We want it to be automatically integrated into our Fake AI Service page once the app is installed.

After we have set up the dependency and the workspace for the two apps, we can define the new enum extension as follows.

enumextension 50150 "ASH Cust. AI Service Providers" extends "ASH Fake AI Service Providers"
{
    value(50150; Apricot)
    {
    }
}
Enum extension of "ASH Fake AI Service Providers" containing Apricot value

The Apricot service provider codeunit needs its own interface implementation and we will complete the integration of the custom app via the event subscription.

codeunit 50150 "ASH Apricot AI Service Mgt." implements "ASH AI Service Provider"
{
    procedure GetAnswer(Question: Text) Answer: Text
    begin
        Answer := 'Apricot says: follow your heart';
    end;
}

codeunit 50151 "ASH Cust. Serv. Provider Mgt."
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"ASH Fake AI Service Mgt.", OnBeforeGetAnswer, '', false, false)]
    local procedure OnBeforeGetAnswer(Provider: Enum "ASH Fake AI Service Providers"; var AIServiceProvider: Interface "ASH AI Service Provider");
    var
        ApricotAIServiceMgt: Codeunit "ASH Apricot AI Service Mgt.";
    begin
        if Provider = Provider::Apricot then
            AIServiceProvider := ApricotAIServiceMgt;
    end;
}
Apricot AI Service Mgt codeunit and the event subscriber code

Once the second app is also published, the custom implementation can now be tested along with the built-in ones, and we get the expected response.

Apricot appears on the Provider list and the response comes from the custom app
Apricot appears on the Provider list and the response comes from the custom app

Phew, a customized code base is no longer required and the future looks brighter. Our Customers are still looking forward to doing business together with us!

What we have done now is applying the Dependency Inversion principle, one of the SOLID principles of software design. Instead of making our base product code dependent and tightly coupled with the customers code, we let customers’ solutions to depend on ours and inverted the dependency. This approach allows us to develop our features without really having to know about the custom logic for service providers. All we needed to do is to let the custom solutions know what our product expects them to do - provide a GetAnswers method implementation - and that we did with the interface definition. All customers needed to do is to have implemented the interface. Everyone can do what they want now without interference as long as the system communication contract is fulfilled.

Mapping interfaces with implementations

Although we’ve sold a couple of program code management problems, our main codeunit "ASH Fake AI Service Mgt." still needs updating when a service provider is added to our base product. There is a neater way to map interfaces with implementations that will let us completely free the codeunit from references to specific AI provider services codeunits and related to them logic leaving only reusable program code. We will look into it next.

The fact is that Enums in AL also can implement interfaces. Well, not exactly but with Enums we can further reference an implementation against each value. There is also the DefaultImplementation Enum property that can be used for all values without a specific Implementation in case we need it. Let’s see how we can use the enum 50100 "ASH Fake AI Service Providers" more effectively.

enum 50100 "ASH Fake AI Service Providers" implements "ASH AI Service Provider"
{
    Extensible = true;

    value(50100; Bingle)
    {
        Implementation = "ASH AI Service Provider" = "ASH Bingle AI Service Mgt.";
    }
    value(50101; Alfasoft)
    {
        Implementation = "ASH AI Service Provider" = "ASH Alfasoft AI Service Mgt.";
    }
}
Enum "ASH Fake AI Service Providers" includes "ASH AI Service Provider" interface implementations

With this approach, we can further clean up our main codeunit 50100 "ASH Fake AI Service Mgt." and remove all the implementation variables which we mapped in the Enum object.

codeunit 50100 "ASH Fake AI Service Mgt."
{
    procedure GetAnswer(Provider: Enum "ASH Fake AI Service Providers"; Question: Text) Answer: Text
    var
        AIServiceProvider: Interface "ASH AI Service Provider";
    begin
        if Question = '' then
            Error('Please enter your question.');

        case Provider of
            Provider::Bingle, Provider::Alfasoft:
                AIServiceProvider := Provider;
            else
                OnBeforeGetAnswer(Provider, AIServiceProvider);
        end;

        Answer := AIServiceProvider.GetAnswer(Question);
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeGetAnswer(Provider: Enum "ASH Fake AI Service Providers"; var AIServiceProvider: Interface "ASH AI Service Provider")
    begin
    end;
}
Main codeunit updated to assigned an enum value to interface variable

The dependent app Custom AI Provider has broken with the change - it also requires the enum extension update.

enumextension 50150 "ASH Cust. AI Service Providers" extends "ASH Fake AI Service Providers"
{
    value(50150; Apricot)
    {
        Implementation = "ASH AI Service Provider" = "ASH Apricot AI Service Mgt.";
    }
}
Enum extension in Custom AI Provider also maps the implementation

With this change, we no longer need the event and the subscriber code, all implementations are now mapped via the "ASH Fake AI Service Providers" enum and we can remove the redundant program code - delete entire codeunit 50151 "ASH Cust. Serv. Provider Mgt." and remove the OnBeforeGetAnswer event and related code from the codeunit 50100 "ASH Fake AI Service Mgt.". The main codeunit looks as lean as never before!

codeunit 50100 "ASH Fake AI Service Mgt."
{
    procedure GetAnswer(Provider: Enum "ASH Fake AI Service Providers"; Question: Text) Answer: Text
    var
        AIServiceProvider: Interface "ASH AI Service Provider";
    begin
        if Question = '' then
            Error('Please enter your question.');

        AIServiceProvider := Provider;
        Answer := AIServiceProvider.GetAnswer(Question);
    end;
}
Event has been removed from the main codeunit

There is actually no essential need for the main codeunit 50100 "ASH Fake AI Service Mgt." now. We can assign the enum to the interface directly in the page action if you wish to go further.

At this point, we have rewritten both of the apps to use Enum as the implementation mapper. The main codeunit no longer needs to know about any implementation details of the service. All it is responsible from now on is only processing whatever we receive from the service calls. If we need to add a new service provider, we would only need to add it to the Enum object and provide the interface implementation. The service page and the main codeunit is now reused across entire functionality when we need to add new service providers or update their system logic.

An example from Business Central apps

Every time I need to learn more about using AL types or find code samples, I tend to look into the standard Microsoft applications first. There is a great example for interfaces there - the Email Connector interface. It’s defined in the System Application and implemented in dedicated to a particular connector apps, for instance, SMTP or Microsoft 365. The below is a screenshot from my environment.

Extension management page with listed Email apps from Microsoft
Extension management page with listed Email apps from Microsoft

When we use the Set Up Email page, where do you think the list items come from?

Set Up Email page with Account Types listed
Set Up Email page with Account Types listed

You’re right, from the installed Email apps. By this time, you already know how those items automatically appear there, well done! If you look into the Business Central applications source code, I’m sure you’ll find something new that we haven’t covered today.

Summary

After making a couple of impractical decisions in the beginning of our AI services app design, I’m glad that we have finally achieved the primary objective of our project. I hope that by gradually making changes to our source code, we all now have a clear understanding that the main purpose of interfaces is being able to separate implementation details from the system parts that need not be concerned with them. Especially, if we have several ways of performing the same task and we want to create a program module or an app for each of them.

We were able to enable our valuable customers to work on their own software modules extending our base product. We still could improve our application without interference due to loose coupling of our apps achieved by applying the Dependency Inversion principle.

With the interfaces concept understood and applied, we have also reduced the number of code lines and made our code more readable and maintainable.

If there are still questions on the subject or I missed anything along the way, please let me know. Happy coding!

The source code for this blog post can be found at https://github.com/ashirokikh/ashirokikh.com