Another Look at App Integration in Business Central – Part 2

James PearsonBusiness Central16 hours ago34 Views

Recap

See here for part 1 of this series. The challenge that we are trying to solve is to allow Business Central apps to call each other’s functionality without creating a dependency.

Reminder: only do this when you cannot create a dependency between the two apps. If a dependency is acceptable then that is the way that you should solve this problem.

Scenario

My scenario was having two apps: Web Shop Integration and Shipping Agent Integration. We cannot afford to create a dependency between the two – we must be able to sell and deploy them independently of one another to our customers. However, if we do deploy both apps into an environment then they must interact with one another.

I finished the previous post with the suggestion that an interface in a shared dependency is a good way to address this requirement.

Example

There is some sample code in this repo: https://github.com/jimmymcp/app-integration-demo. For ease I’ve put the functionality of all three layers into the same workspace, but for real these apps might exist in different repos.

Overview

There are three apps in my example:

  • App Integration
  • Shipping Agent Integration
  • Web Shop Integration

Let’s think about what each of these apps is responsible for. In the previous post, one of my design goals was the separation of concerns. Each app should have a clear set of responsibilities which does not overlap with the responsibilities of another app. You might know this as the single responsibility principle.

App Integration

This app is going to:

  • Hold the interface for shipping agent integration (more of that below)
  • Allow another app to register its implementation of that interface
  • Allow another app to check whether the shipping agent integration is implemented, and specifically which version of that interface is implemented

Shipping Agent Integration

Is going to:

  • Implement the shipping agent integration interface which is defined in the app integration layer i.e. provide the business logic to calculate the shipping charges for a given sales order
  • Register its implementation with the app integration layer

Web Shop Integration

Is going to:

  • Check whether the shipping agent integration interface has is implemented
  • If so, call the method to calculate the shipping charges for a sales order

Interface(s)

IShippingAgentIntegration

The key thing here is the Shipping Agent Interface. This is a contract between the two apps. If there is an implementation of shipping agent integration then this is the functionality that it must provide.

This is a very simple example of what that might look like. A single method which takes a temporary sales header and set of sales lines and returns a decimal.

namespace JamesPearson.AppIntegration;
using Microsoft.Sales.Document;

interface "IShippingAgentIntegration1.0"
{
    procedure CalculateShippingCharge(var TempSalesHeader: Record "Sales Header" temporary; var TempSalesLine: Record "Sales Line" temporary): Decimal
}

That gives the Web Shop Integration app all the information that it needs. It doesn’t know or care how that functionality is provided, only that it is. Equally, Shipping Agent Integration doesn’t need to know anything about the app(s) which are calling that functionality, only that they will provide the specified parameters and handle the return values.

You’ll notice that the interface name includes a version no. We are going to need that when we need to add functionality to the contract. More about that in a future post.

IApp

In addition to the Shipping Agent Integration interface there is also an IApp interface. This interface holds methods which need to be implemented by all apps which provide functionality to other apps through the app integration layer.

The only thing that this interface defines is a method to return the version of the interface which is implemented.

namespace JamesPearson.AppIntegration;

interface IApp
{
    procedure GetVersion(): Version
}

App Enum

Next, I’ve got an enum which lists the apps which expose functionality through the app integration layer. For now, this is only the Shipping Agent Integration app.

namespace JamesPearson.AppIntegration;

enum 50300 App
{
    Extensible = true;
    
    value(0; ShippingIntegration)
    {
        Caption = 'Shipping Integration';
    }
}

This enum is going to be used by both Web Shop Integration to check whether an interface implementation exists and by Shipping Agent Integration to register an implementation.

Flow

The flow between the apps is going to something like this:

  1. The Shipping Integration app registers an implementation of the IShippingAgentIntegration interface OnAfterLogin with the App Integration app
  2. The App Integration app stores the implementation in a dictionary in memory
  3. The Web Integration app will ask the App Integration app whether there is an implementation of the IShippingAgentIntegration interface
  4. If so, the Web Integration app will ask for the implementation so that it can call its method to calculate the shipping charges

Implementation

Having defined the IShippingInterface interface in the App Integration layer the Shipping Agent Integration app now needs to implement it and register its implementation. That might look something like this:

namespace JamesPearson.ShippingAgentIntegration;
using JamesPearson.AppIntegration;
using Microsoft.Sales.Document;
using System.Environment.Configuration;

codeunit 50400 "Shipping Agent Integration" implements IApp, "IShippingAgentIntegration1.0"
{
    procedure GetVersion(): Version
    begin
        exit(Version.Create(1, 0, 0, 0));
    end;

    procedure CalculateShippingCharge(var TempSalesHeader: Record "Sales Header" temporary; var TempSalesLine: Record "Sales Line" temporary): Decimal
    begin
        //business logic for calculating shipping charges
        Randomize(Time() - 0T);
        exit(Random(10));
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"System Initialization", OnAfterLogin, '', false, false)]
    local procedure "System Initialization_OnAfterLogin"()
    var
        AppIntegration: Codeunit "App Integration";
    begin
        AppIntegration.Register(Enum::App::ShippingIntegration, this);
    end;
}

This codeunit implements both the IApp and IShippingIntegration interfaces – that is going to be important in a minute. It returns the version of the IShippingIntegration interface which it is implementing from GetVersion and also implements the shipping charge logic in CalcShippingCharges.

It also has a subscription to the OnAfterLogin event to register its implementation with the App Integration layer, passing a copy of itself with this.

Registering the Implementation

Over to the App Integration layer to store the implementation of the interface that has been passed to it. The App Integration layer has a dictionary of [Enum App, Interface IApp] (the ability to use interfaces in collections has been added recently). This codeunit is SingleInstance to keep the interface implementations in memory for when we need to call them.

namespace JamesPearson.AppIntegration;

codeunit 50300 "App Integration"
{
    SingleInstance = true;

    var
        Apps: Dictionary of [Enum App, Interface IApp];

    procedure Register(App: Enum App; IApp: Interface IApp)
    begin
        if Apps.ContainsKey(App) then
            Apps.Set(App, IApp)
        else
            Apps.Add(App, IApp);
    end;

    procedure GetInterfaceVersion(App: Enum App): Version
    begin
        if not HasImplementation(App) then
            exit(Version.Create(0, 0, 0, 0));

        exit(Apps.Get(App).GetVersion());
    end;

    procedure HasImplementation(App: Enum App): Boolean
    begin
        exit(Apps.ContainsKey(App));
    end;

    procedure "ShippingAgentIntegration1.0"(): Interface "IShippingAgentIntegration1.0"
    begin
        exit(Apps.Get(App::ShippingIntegration) as "IShippingAgentIntegration1.0");
    end;
}

The code should be fairly self-explanatory but:

  • Register allows another app to register an implementation of the interface associated with a particular app in the App enum
  • HasImplementation allows another app to check whether we have an implementation of a certain app’s interface
  • GetInterfaceVersion allows another app to check which version of an app’s interface has been implemented
  • IShippingAgentIntegration1.0 is specifically for the Shipping Agent Integration app and returns the implementation of that interface which it has in the Apps dictionary (casting the interface to the specific type with as)

Consuming the Implementation

All that is left is for the consuming app to test whether we have an implementation of a certain app’s interface and call its functionality if we have.

This is a codeunit in the Web Integration codeunit which is responsible for calculating shipping charges for the order. If Shipping Agent Integration is also installed then we need to ask it to calculate the charges. If it isn’t then we have some alternative logic.

namespace JamesPeason.WebShopIntegration;
using JamesPearson.AppIntegration;
using Microsoft.Sales.Document;

codeunit 50350 "Calc. Shipping Charges"
{
    procedure CalcShippingCharge(var TempSalesHeader: Record "Sales Header" temporary; var TempSalesLine: Record "Sales Line" temporary): Decimal
    var
        AppIntegration: Codeunit "App Integration";
    begin
        //if the shipping agent integration interface is implemented then call its method
        if AppIntegration.GetInterfaceVersion(Enum::App::ShippingIntegration) >= Version.Create(1, 0, 0, 0) then
            exit(AppIntegration."ShippingAgentIntegration1.0"().CalculateShippingCharge(TempSalesHeader, TempSalesLine));

        //if not then we have some alternative logic to calculate shipping charges
        exit(SomeAlternativeLogic(TempSalesHeader, TempSalesLine));
    end;

    local procedure SomeAlternativeLogic(var TempSalesHeader: Record "Sales Header" temporary; var TempSalesLine: Record "Sales Line" temporary): Decimal
    begin
        //some alternative logic for calculating shipping charges goes here
    end;
}

The Web Integration app asks the App Integration app whether we have at least v1.0 of the Shipping Agent Integration interface. If we do, then it retrieves the implementation and calls

Conclusions

There are few moving parts to support this design, but it achieves the key design goals of allowing the two apps to integrate with one another without requiring a dependency between them.

The interface provides a definite contract of the functionality will be implemented. When we want to change that contract we can create a new version of the interface (in fact, we will have to in order to avoid breaking changes). More of that in another post.

Check james’s original post https://jpearson.blog/2025/05/07/another-look-at-app-integration-in-business-central-part-2/ on jpearson.blog which was published 2025-05-07 18:08:00

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Join Us
  • X Network2.1K
  • LinkedIn3.8k
  • Bluesky0.5K
Support The Site
Events
May 2025
MTWTFSS
    1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
« Apr   Jun »
Follow
Sign In/Sign Up Sidebar Search
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...