Si tienes clientes de Business Central on-premise, sabes que no todos están en la misma versión. Uno sigue en la v22, otro acaba de subir a la v24 y el de SaaS ya va por la v26. Y tu extensión tiene que funcionar en los tres.
La salida fácil es una rama de Git por versión. Funciona el primer mes; después, cada corrección hay que llevarla a mano a tres ramas, y alguna se olvida. La alternativa es una sola base de código que se compila distinta según la versión de destino, con las directivas de preprocesador de AL.
Qué son las directivas de preprocesador
Desde Business Central 2020 release wave 2 (v17), el compilador de AL entiende líneas que empiezan por #. No son código: se evalúan antes de compilar y deciden qué trozos llegan al compilador y cuáles se descartan.
| Directiva | Para qué sirve |
|---|---|
#if |
Empieza un bloque condicional: el código entra si la expresión es verdadera. |
#elif |
Otra condición, si ninguna anterior se ha cumplido. |
#else |
El código que entra si no se ha cumplido ninguna. |
#endif |
Cierra el bloque. |
#define / #undef |
Define o quita un símbolo, solo en ese fichero. |
Las condiciones admiten and, or, not y paréntesis. Se puede hacer condicional cualquier cosa: una línea dentro de un procedimiento, un procedimiento entero, un campo de una tabla o un objeto completo.
Los símbolos: interruptores encendidos o apagados
Un símbolo es un nombre que está definido (vale true) o no (vale false). No tiene valor numérico: no existe #if VERSION >= 24.
Los símbolos para toda la extensión se declaran en app.json:
{
"preprocessorSymbols": [ "BC22PLUS", "BC24PLUS" ]
}
Y con #define se pueden añadir solo para un fichero, siempre antes del primer objeto:
#define DEBUG
codeunit 50100 "CM Diagnostics"
{
...
}
Un detalle importante: AL no trae símbolos de versión de serie. El compilador no sabe "para qué versión estás compilando" a efectos de #if; esos símbolos los defines tú.
Símbolos acumulativos: el truco para comparar versiones
Como no hay comparaciones, el patrón que mejor funciona es definir un símbolo por cada versión mínima que se cumple. Al compilar para la v26, se encienden también los de la v22 y la v24:
Así, #if BC24PLUS significa "de la v24 en adelante", y #if not BC24PLUS, "anterior a la v24". Es el mismo enfoque que usa Microsoft en la Base App con sus símbolos CLEAN22, CLEAN23... para el código que va a eliminar.
Un ejemplo real: las series numéricas en la v24
En la v24 (2024 release wave 1), Microsoft marcó como obsoleta la codeunit NoSeriesManagement y la sustituyó por la nueva "No. Series". Si tu extensión numera documentos propios, en la v24 te salta el aviso de obsoleto y, cuando Microsoft la elimine, dejará de compilar. Pero en la v22 la codeunit nueva todavía no existe.
Con directivas, el mismo fichero sirve para todas:
table 50100 "CM Service Order"
{
...
trigger OnInsert()
var
ServiceSetup: Record "CM Service Setup";
#if BC24PLUS
NoSeries: Codeunit "No. Series";
#else
NoSeriesMgt: Codeunit NoSeriesManagement;
#endif
begin
if "No." <> '' then
exit;
ServiceSetup.Get();
#if BC24PLUS
"No. Series" := ServiceSetup."Service Order Nos.";
"No." := NoSeries.GetNextNo("No. Series", WorkDate());
#else
NoSeriesMgt.InitSeries(ServiceSetup."Service Order Nos.", xRec."No. Series", WorkDate(), "No.", "No. Series");
#endif
end;
}
Al compilar para la v22, el compilador solo ve la rama de NoSeriesManagement; para la v24 o la v26, solo la de "No. Series". Ninguna de las dos versiones del .app arrastra código que no entiende.
También se pueden condicionar campos y objetos enteros, por ejemplo una pageextension sobre una página que solo existe a partir de cierta versión:
#if BC26PLUS
pageextension 50110 "CM New Feature Page" extends "Some Page Added In 26"
{
...
}
#endif
Silenciar avisos en la rama antigua
Si una versión intermedia todavía tiene el objeto antiguo marcado como obsoleto, el compilador lanzará el aviso AL0432. Cuando conscientemente lo sigues usando en la rama de compatibilidad, #pragma lo silencia solo en ese trozo:
#pragma warning disable AL0432
NoSeriesMgt.InitSeries(...);
#pragma warning restore AL0432
Cómo cambiar de versión: app.json por destino
El app.json no admite directivas, y es justo el fichero que más cambia entre versiones: application, platform, runtime, las dependencias y los símbolos.
La solución práctica es guardar los valores de cada destino y generar el app.json antes de compilar. Por ejemplo, con un pequeño script de PowerShell:
param([ValidateSet('22', '24', '26')] [string] $Version = '26')
$targets = @{
'22' = @{ application = '22.0.0.0'; runtime = '11.0'; symbols = @('BC22PLUS') }
'24' = @{ application = '24.0.0.0'; runtime = '13.0'; symbols = @('BC22PLUS', 'BC24PLUS') }
'26' = @{ application = '26.0.0.0'; runtime = '15.0'; symbols = @('BC22PLUS', 'BC24PLUS', 'BC26PLUS') }
}
$t = $targets[$Version]
$app = Get-Content app.json -Raw | ConvertFrom-Json
$app.application = $t.application
$app.platform = $t.application -replace '^(\d+)\..*', '$1.0.0.0'
$app.runtime = $t.runtime
$app.preprocessorSymbols = $t.symbols
$app | ConvertTo-Json -Depth 10 | Set-Content app.json -Encoding utf8
En Visual Studio Code, usa además una carpeta de símbolos por versión (.alpackages/bc22, .alpackages/bc24...) con el ajuste al.packageCachePath, para no descargar los símbolos cada vez que cambias de destino.
Si compilas con AL-Go for GitHub, no hace falta el script: tiene el ajuste preprocessorSymbols en su settings.json, combinable con configuraciones condicionales para lanzar un build por versión.
Buenas prácticas
- Compila todos los destinos en cada cambio. Una rama
#ifque nadie compila se rompe sin que te enteres. En el pipeline, un build por versión soportada. - Pocas ramas y pequeñas. Condiciona la línea o el procedimiento que cambia, no medio fichero. Si un bloque crece, sácalo a un procedimiento propio con dos implementaciones.
- Nombres claros y documentados.
BC24PLUSse entiende solo;NEWNOSERIES, no tanto. Apunta en el README qué símbolos existen y qué significan. - Limpia cuando dejes una versión. Cuando ya no des soporte a la v22, busca
BC22PLUS, quita las ramas#elsey el símbolo. Las directivas son deuda técnica con fecha de caducidad. - Las ramas de Git, para lo que no cabe. Si dos versiones divergen en casi todo (por ejemplo, NAV con C/AL frente a BC con AL), las directivas no son la herramienta: ahí sí tiene sentido separar.
Directivas o ramas de Git
| Ramas por versión | Una base con directivas | |
|---|---|---|
| Corregir un error | En cada rama, a mano | Una vez |
| Ver qué cambia entre versiones | Comparando ramas | En el propio código |
| Riesgo de que diverjan | Alto | Bajo |
| Complejidad del código | Baja | Algo mayor (bloques #if) |
| Build | Uno por rama | Uno por destino desde la misma rama |
Para una extensión que vive en varios clientes con versiones distintas, la base única gana casi siempre.
Resumen
- Las directivas (
#if,#elif,#else,#endif) deciden qué código se compila. - Los símbolos se definen en
preprocessorSymbolsdeapp.jsono con#define; AL no trae símbolos de versión. - Usa símbolos acumulativos (
BC22PLUS,BC24PLUS...) para expresar "de esta versión en adelante". - Genera el
app.jsonde cada destino con un script o con AL-Go, y compila todas las versiones en cada cambio.
Fuente: AL Preprocessor Directives Overview – Microsoft Learn.
