← Todos los artículos/ blog ·

Una sola base de código AL para varias versiones de Business Central

Cómo mantener una extensión que compila para BC 22, BC 24 y BC 26 desde el mismo repositorio con directivas de preprocesador (#if,

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:

Símbolos acumulativos por versión de destino: BC22PLUS, BC24PLUS y BC26PLUS

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.

Flujo: un repositorio con directivas compila tres .app distintos para BC 22, BC 24 y BC 26

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

  1. Compila todos los destinos en cada cambio. Una rama #if que nadie compila se rompe sin que te enteres. En el pipeline, un build por versión soportada.
  2. 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.
  3. Nombres claros y documentados. BC24PLUS se entiende solo; NEWNOSERIES, no tanto. Apunta en el README qué símbolos existen y qué significan.
  4. Limpia cuando dejes una versión. Cuando ya no des soporte a la v22, busca BC22PLUS, quita las ramas #else y el símbolo. Las directivas son deuda técnica con fecha de caducidad.
  5. 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 preprocessorSymbols de app.json o 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.json de cada destino con un script o con AL-Go, y compila todas las versiones en cada cambio.

Fuente: AL Preprocessor Directives Overview – Microsoft Learn.

¿Te encaja en tu proyecto?

Si quieres aplicar algo de esto en tu web o en tu Business Central, hablemos.