How to fix error: "Cannot edit a record in Azure storage accounts (DocAzureStorageAccount)"

There is a potential bug when transferring databases between environments that can give the following error message:

Database upgrade to Docentric version at 27.09.2023 16:01:30:

Error occurred while clearing data after changed environment hostname.: Microsoft.Dynamics.Ax.Xpp.ErrorException: Cannot edit a record in Azure storage accounts (DocAzureStorageAccount).

Update must be performed inside a transaction.

at Microsoft.Dynamics.Ax.MSIL.Interop.throwException(Int32 ExceptionValue, interpret* ip)

at Microsoft.Dynamics.Ax.MSIL.cqlCursorIL.update(IntPtr table)

at Microsoft.Dynamics.Ax.Xpp.Common.__Update_IntImpl__() in D:\dbs\sh\l23t\0822_045345\cmd\e\Source\Kernel\xppil\XppSupport\XppCommon.cs:line 1818

at Microsoft.Dynamics.Ax.Xpp.Common.Update() in D:\dbs\sh\l23t\0822_045345\cmd\e\Source\Kernel\xppil\XppSupport\XppCommon.cs:line 1811

at Dynamics.AX.Application.DocAzureStorageAccount.`update() in xppSource://Source/Docentric AX\AxTable_DocAzureStorageAccount.xpp:line 28

at Dynamics.AX.Application.DocAzureStorageAccount.update()

at Dynamics.AX.Application.DocAzureStorageAccount.`clearEncryptedFields() in xppSource://Source/Docentric

The underlying Docentric code hasn’t changed and this bug only started happening from around D365FO version 10.0.34.

This bug will be fixed in the hotfix release 3.4.7.1.

In the meantime, if it’s presenting a pressing issue it can be fixed by replacing the code of the clearEncryptedFields() method in the DocAzureStorageAccount class with the following:

    /// <summary>
    /// Clears encrypted fields (ConnectionString)).
    /// </summary>
    /// <returns>Description od changed fields if some records were changed; otherwise empty string.</returns>
    public static str clearEncryptedFields()
    {
        DocAzureStorageAccount  docAzureStorageAccount;
        UserConnection          userConnection = new UserConnection();
        boolean                 changed = false;
        
        docAzureStorageAccount.setConnection(userConnection);

        docAzureStorageAccount.ttsBegin();

        while select forupdate docAzureStorageAccount
        {
            // clear invalid ConnectionString fields (exception thrown at decryption)
            str value;
            int startLine = infologLine();

            if (!UL.ReflectionHelper::TryInvokeInstanceMethod(byref value, docAzureStorageAccount, tableMethodStr(DocAzureStorageAccount, getConnectionString)))
            {
                // clear swallowed exception messages
                if (startLine < infologLine())
                {
                    infoLog.cut(startLine + 1);
                }

                docAzureStorageAccount.connectionStringEdit(true, '');
                docAzureStorageAccount.update();
                changed = true;
            }
        }

        docAzureStorageAccount.ttsCommit();

        return (changed ? 'Azure storage accounts (DocAzureStorageAccount) connection strings' : '');
    }

The above code fix will work if you’re using Docentric version 3.4.7. If you’re using an older version, you will need to either upgrade or do the following:

  1. Copy the DocStringSplitOptions (linked here) into the C:\AOSService\PackagesLocalDirectory\DocentricAX\Docentric AX\AxEnum directory.
  2. Add the following code to the end of the DocGlobalHelper class:
    /// <summary>
    /// Splits a string into a list of substrings delimited by elements in the specified delimiter string.
    /// This is done using the specified string splitting options, which can be:
    /// - None - the default mode, which doesn't trim the substrings nor omit empty substrings from the resulting list
    /// - RemoveEmptyEntries - omit all substrings that contain an empty string from the resulting list
    /// - TrimEntries - trim white-space characters from each substring in the result
    /// If RemoveEmptyEntries and TrimEntries are specified together, then substrings that consist only of
    /// white-space characters are also removed from the result.
    /// </summary>
    /// <param name = "_stringToSplit">The string to split into a list.</param>
    /// <param name = "_delimiters">A string of delimiter characters.</param>
    /// <param name = "_stringSplitOptions">
    /// A bitwise combination of the DocStringSplitOptions enum values that specifies whether to trim
    /// substrings and include empty substrings.
    /// </param>
    /// <returns>A list of substrings from the _stringToSplit parameter.</returns>
    /// <remarks>
    /// Each character in the _delimiter string is used to split the _stringToSplit parameter.
    /// The code is modified version of Global::strSplit() method.
    /// </remarks>
    public static List strSplit(str _stringToSplit, str _delimiters, int _stringSplitOptions = DocStringSplitOptions::None)
    {
        List list = new List(Types::String);
        int oldPos = 1;
        int pos;
        int strLength = strLen(_stringToSplit);
 
        do
        {
            // Find next position of the delimiter in the string
            pos = strFind(_stringToSplit, _delimiters, oldPos, strLength);
            if (!pos)
            {
                pos = strLength + 1;
            }
 
            // Get a substring from the string
            str s = subStr(_stringToSplit, oldPos, pos-oldPos);
 
            // Trim a substring depending on the string splitting options
            //if (_stringSplitOptions == DocStringSplitOptions::TrimEntries || 
            //        _stringSplitOptions == (DocStringSplitOptions::RemoveEmptyEntries | DocStringSplitOptions::TrimEntries))
            if (_stringSplitOptions & DocStringSplitOptions::TrimEntries)
            {
                s = strLRTrim(s);
            }
 
            // Add a substring to the list depending on the string splitting options
            //if (!(s == '' && (_stringSplitOptions == DocStringSplitOptions::RemoveEmptyEntries ||
            //        _stringSplitOptions == (DocStringSplitOptions::RemoveEmptyEntries | DocStringSplitOptions::TrimEntries))))
            if (!(s == '' && (_stringSplitOptions & DocStringSplitOptions::RemoveEmptyEntries)))
            {
                // Add a substring to the list
                list.addEnd(s);
            }
            oldPos = pos+1;
        }
        while (pos <= strLength);
 
        return list;
    }