AL extensions are how you customise Microsoft Dynamics 365 Business Central. Getting one to compile is easy. Getting one that survives years of Business Central updates, performs well with large data volumes and can be supported by other developers takes some discipline.
These are the practices I follow on every Business Central project, whether it's a small per-tenant extension or a large solution.
1. Extend, don't modify: design around events
In Business Central you can't change standard objects. You extend them with table extensions, page extensions, report extensions and event subscribers. This is what makes the platform upgradeable, so lean into it.
enum 50100 "DS Loyalty Tier"
{
Extensible = true;
value(0; Standard) { Caption = 'Standard'; }
value(1; Silver) { Caption = 'Silver'; }
value(2; Gold) { Caption = 'Gold'; }
}
codeunit 50110 "DS Sales Post Subscribers"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', false, false)]
local procedure CheckLoyaltyTierOnBeforePost(var SalesHeader: Record "Sales Header")
var
Customer: Record Customer;
MissingTierErr: Label 'Customer %1 must have a loyalty tier before posting.', Comment = '%1 = Customer No.';
begin
if SalesHeader."Document Type" <> SalesHeader."Document Type"::Order then
exit;
Customer.SetLoadFields("DS Loyalty Tier");
Customer.Get(SalesHeader."Sell-to Customer No.");
if Customer."DS Loyalty Tier" = Enum::"DS Loyalty Tier"::Standard then
Error(MissingTierErr, Customer."No.");
end;
}
- Subscribers only need to declare the parameters they actually use.
- Be careful with
IsHandledevents. Skipping standard code is powerful, but it also means you're responsible for everything the standard code did. - Publish your own integration events so other extensions, including your own future ones, can extend you too.
- Use enums and interfaces instead of hard-coded
casestatements when behaviour needs to vary.
2. Naming, affixes and object IDs
Every object and every field you add to a standard table should carry a prefix or suffix (for example DS), so it can't collide with Microsoft's objects or other apps. AppSource requires it, and it's good practice for per-tenant extensions too. Keep object IDs inside the ranges declared in app.json:
{
"id": "6f1c3a52-2c7e-4c0e-9d0a-1f5b2c3d4e5f",
"name": "DS Loyalty",
"publisher": "Diwas Shrestha",
"version": "1.0.0.0",
"brief": "Customer loyalty tiers for Business Central",
"platform": "1.0.0.0",
"application": "26.0.0.0",
"runtime": "15.0",
"idRanges": [{ "from": 50100, "to": 50149 }],
"features": ["NoImplicitWith", "TranslationFile"],
"applicationInsightsConnectionString": "InstrumentationKey=…;IngestionEndpoint=…"
}
Set application to the lowest Business Central version you support, not just the one on your machine.
3. Write performance-aware AL
Most slow Business Central customisations come down to a handful of patterns:
- Use
SetLoadFields(partial records) so the database only reads the columns you use. This matters a lot on wide tables like Customer, Item and Sales Line. - Filter before you loop. Use
SetRangeandSetFilteron indexed fields, and add keys for your common filters. - Pick the right read. Use
FindSetfor loops,FindFirstorGetfor single records,IsEmptyfor existence checks, andCount,CalcSumsorModifyAllinstead of manual loops. - Avoid
Commitinside loops and long-running transactions that lock tables for other users. - Move heavy work to the background with job queue entries,
TaskScheduleror page background tasks.
local procedure NotifyGoldCustomers()
var
Customer: Record Customer;
begin
Customer.SetLoadFields("No.", Name, "E-Mail");
Customer.SetRange("DS Loyalty Tier", Enum::"DS Loyalty Tier"::Gold);
if Customer.FindSet() then
repeat
SendLoyaltyMail(Customer);
until Customer.Next() = 0;
end;
4. Turn on the code analyzers
The AL compiler ships with analyzers that catch problems before your users do. Enable them in .vscode/settings.json and treat warnings as work to do, not noise:
{
"al.enableCodeAnalysis": true,
"al.codeAnalyzers": ["${CodeCop}", "${UICop}", "${PerTenantExtensionCop}"],
"al.ruleSetPath": "./project.ruleset.json"
}
Use AppSourceCop instead of PerTenantExtensionCop if you publish to AppSource. It also checks for breaking changes against the previous version of your app.
5. Classify data and log telemetry
- Set
DataClassificationon every field (for exampleCustomerContentorEndUserIdentifiableInformation) so privacy tooling and GDPR requests work correctly. - Add an Application Insights connection string to
app.jsonand log important events withSession.LogMessage. When something fails in production, telemetry tells you what happened without a debugger.
6. Write automated tests
Test codeunits run inside Business Central and can drive real business logic. Use the given/when/then style and the standard test libraries (add the test toolkit apps as dependencies of your test app):
codeunit 50190 "DS Loyalty Tests"
{
Subtype = Test;
var
Assert: Codeunit "Library Assert";
LibrarySales: Codeunit "Library - Sales";
[Test]
procedure PostingFailsWithoutLoyaltyTier()
var
Customer: Record Customer;
SalesHeader: Record "Sales Header";
begin
// [GIVEN] A customer on the default tier
LibrarySales.CreateCustomer(Customer);
// [WHEN] A sales order for that customer is posted
LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No.");
asserterror LibrarySales.PostSalesDocument(SalesHeader, true, true);
// [THEN] Posting is blocked with a clear message
Assert.ExpectedError('must have a loyalty tier');
end;
}
7. Plan for install, upgrade and CI/CD
- Use install codeunits (
Subtype = Install) to set up default data, and upgrade codeunits with upgrade tags when your schema changes between versions. - Never remove or rename published fields without a deprecation path (
ObsoleteState = Pending, thenRemoved). - Automate builds with AL-Go for GitHub or Azure DevOps pipelines. Compile, run the analyzers, run the tests and sign the app on every pull request.
- Keep everything in Git, including the
app.jsonversion you bump on each release.
Quick checklist
| Area | Ask yourself |
|---|---|
| Design | Am I using events and extensions, with no copies of standard objects? |
| Naming | Do all objects and added fields have my affix and IDs inside my range? |
| Performance | Partial records, filtered loops, no commits in loops? |
| Quality | Analyzers clean, tests green, telemetry wired up? |
| Lifecycle | Install and upgrade codeunits, obsoletion path, automated pipeline? |
Wrapping up
Good AL is mostly about respecting the platform: extend through events, keep your footprint clearly named, read only the data you need, and let analyzers, tests and pipelines catch mistakes early. Your extension will then keep working through every Business Central release.
Looking for a Business Central developer to build or review your extensions? Get in touch.