แก้ไข

Change a module in the System Application

Update an existing System Application module in BCApps by changing its implementation, exposing supported functionality through its public API, and adding tests. This article uses a historical Base64 Convert contribution to illustrate that workflow.

Prerequisites for changing a module

Your changes must follow the module architecture guidelines. Learn more in Module architecture. Preserve the existing public API and its behavior. Run the existing tests and add tests for functionality that you change or add.

Prepare your module development environment

Use the BCApps local development workflow to create a Business Central container and configure Visual Studio Code. Its setup script generates the launch.json and settings.json files for your environment. Follow the repository's current instructions rather than copying connection or assembly paths from an older example.

Open the module and its test project. For Base64 Convert, the folders are:

  • src\System Application\App\Base64 Convert
  • src\System Application\Test\Base64 Convert

If required symbols are missing, in Visual Studio Code, press F1 and select AL: Download Symbols.

The following historical example uses .NET interoperability. If you configure it manually, set al.assemblyProbingPaths in .vscode/settings.json to the required assembly locations. For current BCApps development, use the settings generated by the repository's setup script.

Create a contribution branch

In your local clone, run git checkout -b "<FeatureBranchName>". Replace <FeatureBranchName> with a descriptive name for your change.

Historical example of changing Base64 Convert

The following example is based on ALAppExtensions pull request 7676, which added support for text encodings other than UTF8. For a new contribution, follow the same sequence: update the implementation, expose the public API, and test the behavior.

Important

These overloads and tests already exist. Don't add them again. The example shows the historical .NET-based implementation. The implementation changed to use the native Base64Convert system codeunit. Use the latest Base64 Convert implementation when preparing a new change.

The AL snippets are fragments from existing objects, not standalone objects. They rely on declarations, .NET aliases, and test labels in the surrounding source.

Update the implementation codeunit

Start with the internal implementation codeunit. In the historical contribution, the following overloads were added to Base64ConvertImpl.Codeunit.al. The file is now under src\System Application\App\Base64 Convert\src.

    procedure ToBase64(String: Text; TextEncoding: TextEncoding): Text
    begin
        exit(ToBase64(String, false, TextEncoding, 0));
    end;

    procedure ToBase64(String: Text; TextEncoding: TextEncoding; Codepage: Integer): Text
    begin
        exit(ToBase64(String, false, TextEncoding, Codepage));
    end;

    procedure ToBase64(String: Text; InsertLineBreaks: Boolean; TextEncoding: TextEncoding): Text
    begin
        exit(ToBase64(String, InsertLineBreaks, TextEncoding, 0));
    end;

    procedure ToBase64(String: Text; InsertLineBreaks: Boolean; TextEncoding: TextEncoding; Codepage: Integer): Text
    var
        Convert: DotNet Convert;
        Encoding: DotNet Encoding;
        Base64FormattingOptions: DotNet Base64FormattingOptions;
        Base64String: Text;
    begin
        if String = '' then
            exit('');

        if InsertLineBreaks then
            Base64FormattingOptions := Base64FormattingOptions.InsertLineBreaks
        else
            Base64FormattingOptions := Base64FormattingOptions.None;
        case TextEncoding of
            TextEncoding::UTF16:
                Base64String := Convert.ToBase64String(Encoding.Unicode().GetBytes(String), Base64FormattingOptions);
            TextEncoding::MSDos,
            TextEncoding::Windows:
                Base64String := Convert.ToBase64String(Encoding.GetEncoding(CodePage).GetBytes(String), Base64FormattingOptions);
            else
                Base64String := Convert.ToBase64String(Encoding.UTF8().GetBytes(String), Base64FormattingOptions);
        end;

        exit(Base64String);
    end;

    procedure FromBase64(Base64String: Text; TextEncoding: TextEncoding): Text
    begin
        exit(FromBase64(Base64String, TextEncoding, 1252));
    end;

    procedure FromBase64(Base64String: Text; TextEncoding: TextEncoding; CodePage: Integer): Text
    var
        Convert: DotNet Convert;
        Encoding: DotNet Encoding;
        OutputString: Text;
    begin
        if Base64String = '' then
            exit('');

        case TextEncoding of
            TextEncoding::UTF16:
                OutputString := Encoding.Unicode().GetString(Convert.FromBase64String(Base64String));
            TextEncoding::MSDos,
            TextEncoding::Windows:
                OutputString := Encoding.GetEncoding(CodePage).GetString(Convert.FromBase64String(Base64String));
            else
                OutputString := Encoding.UTF8().GetString(Convert.FromBase64String(Base64String));
        end;
        exit(OutputString);
    end;

The encoding and decoding overloads use different default code pages. When you use MSDos or Windows, specify the same code page explicitly for both operations.

The existing overloads in the implementation codeunit were updated to call the new overloads with the same default encoding:

    procedure ToBase64(String: Text): Text
    begin
        exit(ToBase64(String, false));
    end;

    procedure ToBase64(String: Text; InsertLineBreaks: Boolean): Text
    begin
        exit(ToBase64(String, InsertLineBreaks, TextEncoding::UTF8, 0));
    end;

    procedure FromBase64(Base64String: Text): Text
    begin
        exit(FromBase64(Base64String, TextEncoding::UTF8, 0));
    end;

These forwarding methods preserve the existing UTF8 defaults. When changing a module, keep existing callers in mind and use tests to check compatibility.

Expose and document the public API

Next, expose the functionality through the facade codeunit and document its public procedures. In the historical contribution, the following procedures were added to Base64Convert.Codeunit.al. The file is now under src\System Application\App\Base64 Convert\src. Each procedure calls the corresponding implementation procedure.

The containing facade declares Base64ConvertImpl as a variable of type Codeunit "Base64 Convert Impl.". Learn more in the Base64 Convert facade source.

    /// <summary>
    /// Converts the value of the input string to its equivalent string representation that is encoded with base-64 digits.
    /// </summary>
    /// <param name="String">The string to convert.</param>
    /// <param name="TextEncoding">The TextEncoding for the input string.</param>
    /// <returns>The string representation, in base-64, of the input string.</returns>
    procedure ToBase64(String: Text; TextEncoding: TextEncoding): Text
    begin
        exit(Base64ConvertImpl.ToBase64(String, TextEncoding));
    end;

    /// <summary>
    /// Converts the value of the input string to its equivalent string representation that is encoded with base-64 digits.
    /// </summary>
    /// <param name="String">The string to convert.</param>
    /// <param name="TextEncoding">The TextEncoding for the input string.</param>
    /// <param name="Codepage">The Codepage if TextEncoding is MsDos or Windows.</param>
    /// <returns>The string representation, in base-64, of the input string.</returns>
    procedure ToBase64(String: Text; TextEncoding: TextEncoding; Codepage: Integer): Text
    begin
        exit(Base64ConvertImpl.ToBase64(String, TextEncoding, Codepage));
    end;

    /// <summary>
    /// Converts the specified string, which encodes binary data as base-64 digits, to an equivalent regular string.
    /// </summary>
    /// <param name="Base64String">The string to convert.</param>
    /// <param name="TextEncoding">The text encoding used to decode the converted bytes.</param>
    /// <returns>Regular string that is equivalent to the input base-64 string.</returns>
    /// <error>The length of Base64String, ignoring white-space characters, is not zero or a multiple of 4.</error>
    /// <error>The format of Base64String is invalid. Base64String contains a non-base-64 character, more than two padding characters,
    /// or a non-white space-character among the padding characters.</error>
    procedure FromBase64(Base64String: Text; TextEncoding: TextEncoding): Text
    begin
        exit(Base64ConvertImpl.FromBase64(Base64String, TextEncoding));
    end;

    /// <summary>
    /// Converts the specified string, which encodes binary data as base-64 digits, to an equivalent regular string.
    /// </summary>
    /// <param name="Base64String">The string to convert.</param>
    /// <param name="TextEncoding">The text encoding used to decode the converted bytes.</param>
    /// <param name="Codepage">The Codepage if TextEncoding is MsDos or Windows.</param>
    /// <returns>Regular string that is equivalent to the input base-64 string.</returns>
    /// <error>The length of Base64String, ignoring white-space characters, is not zero or a multiple of 4.</error>
    /// <error>The format of Base64String is invalid. Base64String contains a non-base-64 character, more than two padding characters,
    /// or a non-white space-character among the padding characters.</error>
    procedure FromBase64(Base64String: Text; TextEncoding: TextEncoding; Codepage: Integer): Text
    begin
        exit(Base64ConvertImpl.FromBase64(Base64String, TextEncoding, Codepage));
    end;

Add tests for the changed behavior

Run the existing tests to check compatibility, and then add and run tests for the new behavior. The historical contribution included the following UTF16 tests in Base64ConvertTest.Codeunit.al. The file is now under src\System Application\Test\Base64 Convert\src.

The containing test codeunit supplies the Base64Convert and Assert variables, sample text, expected Base64 value, and error labels. Learn more in the Base64 Convert test source.

    [Test]
    procedure StringToBase64UTF16Test()
    var
        ConvertedText: Text;
    begin
        // [SCENARIO] A string variable is converted to base-64 string, binary representation is kept in UTF16

        // [WHEN] The string is converted
        ConvertedText := Base64Convert.ToBase64(SampleUTF16Txt, TextEncoding::UTF16);

        // [THEN] The converted value is correct
        Assert.AreEqual(Base64SampleUTF16Txt, ConvertedText, ConvertionToBase64UTF16Err);
    end;

    [Test]
    procedure FromBase64UTF16StringTest()
    var
        ConvertedText: Text;
    begin
        // [SCENARIO] A base-64 string with UTF16 encoding is converted to a regular string

        // [WHEN] The base-64 string is converted
        ConvertedText := Base64Convert.FromBase64(Base64SampleUTF16Txt, TextEncoding::UTF16);

        // [THEN] The converted value is correct
        Assert.AreEqual(SampleUTF16Txt, ConvertedText, ConvertionFromBase64UTF16Err);
    end;

Submit your module changes

Build the changed app and its test app, and run the relevant tests before submitting your changes. Follow the BCApps local development guidelines for your environment.

  1. Run git status and review your changes.
  2. Stage each intended file with git add "<ChangedFile>". Replace <ChangedFile> with its path.
  3. Commit your changes with git commit -m "<CommitMessage>". Replace <CommitMessage> with a summary of the change.
  4. Push your branch with git push --set-upstream origin "<FeatureBranchName>". The origin remote must point to your fork.
  5. Open a pull request from your branch to the BCApps repository. Select the appropriate target branch and link it to your approved issue by including Fixes #<IssueNumber> in the description.

Follow the repository's contribution checks and review process. Complete the Contributor License Agreement steps if the pull request bot requests them.

BCApps contribution guidelines
BCApps local development environment guidelines
Module architecture
Create a new module