Saturday, March 22, 2025

Fix build error at "using Microsoft.IdentityModel.Clients.ActiveDirectory" in D365FO update 10.0.43

If you used Azure Active Directory Authentication Library (ADAL) in your customization, and get a compilation error at "using Microsoft.IdentityModel.Clients.ActiveDirectory" statement in 10.0.43 update -- this is because ADAL is deprecated and MS have removed the reference from the AOT. 

You will need to migrate you code to Microsoft Authentication Library (MSAL).

In order to do that, you will need to add the Microsoft.Identity.Client.dll library to your bin-folder (can be found in other subfolders under K:\AosService\PackagesLocalDirectory), and add a corresponding reference to a VS project. The reference will then be created as an XML-file. Both the DLL and the XML files must be added to source control via "Add Items to Folder...", like this:

K:\AosService\PackagesLocalDirectory\<package name>\bin\Microsoft.Identity.Client.dll

K:\AosService\PackagesLocalDirectory\<package name>\<model name>\AxReference\Microsoft.Identity.Client.xml

Then, you will need to change the code.

Before:

using Microsoft.IdentityModel.Clients.ActiveDirectory;

...

            ClientCredential clientCrendential = new ClientCredential(clientId, clientSecret);

            AuthenticationContext authContext = new AuthenticationContext(authority);

            AuthenticationResult authResult = authContext.AcquireToken(resource, clientCrendential);

            accessToken = authResult.AccessToken;

...

After:

using Microsoft.Identity.@Client;

...

            ConfidentialClientApplicationBuilder clientApplicationbuilder = ConfidentialClientApplicationBuilder::Create(clientId);

            IConfidentialClientApplication app = clientApplicationbuilder

                .WithClientSecret(clientSecret)

                .WithAuthority(authority)

                .Build();

            System.Collections.Generic.List<System.String> scopes = new System.Collections.Generic.List<System.String>();

            scopes.Add(resource + '/.default');

            System.Collections.IEnumerable enumerable = scopes;

            AcquireTokenForClientParameterBuilder parameterBuilder = app.AcquireTokenForClient(enumerable);

            var task = parameterBuilder.ExecuteAsync();

            task.Wait();

            AuthenticationResult authResult = task.Result;

            accessToken = authResult.AccessToken;

...


Wednesday, December 7, 2022

D365FO: Bypassing "next" in CoC

DISCLAIMER: Use this trick at your own risk. Do not do this, unless you absolutely have to. 

Some time ago I had to figure out why one of the custom services was slow. It took like 30+ seconds to return a dozen of trade agreements for a customer.

It turned out the SysExtension framework tried to collect data about classes through reflection, and it did that multiple times in the same session. 

The issue may be reproduced with this runnable class. Here, we instantiate instances of 4 different classes. The class constructors use SysExtension framework for figuring out which class type to build.


If running the class the first time, the output would look something like this:

SalesLineType::construct: 13610 ms
SalesSummaryFields::construct: 1968 ms
SalesQuantity::construct: 2078 ms
TradeModuleType::newFromModule: 3406 ms

After System administration -> Setup -> Refresh elements, the time would drop to:

SalesLineType::construct: 1735 ms
SalesSummaryFields::construct: 1781 ms
SalesQuantity::construct: 1641 ms
TradeModuleType::newFromModule: 1656 ms

But still, almost 2 seconds for every single class instance is too much. 

The following change can fix this issue by skipping metadata collection, if it is already done:


This code does the following: if the reflection is already collected (reflectionDataEnumerator.moveNext() returns "true"), we throw an exception of a type, that will most likely never be thrown from the "next populateFromMetadata();" itself. Then, we catch the exception, but that also means the "next" statement is skipped and the reflection cache data is not built from scratch again. 

After this change, the runnable class output looks like this:

SalesLineType::construct: 1657 ms
SalesSummaryFields::construct: 218 ms
SalesQuantity::construct: 125 ms
TradeModuleType::newFromModule: 157 ms

It is important, that this "skip" is only done when there is no transaction scope around, otherwise it would abort the transaction, therefore the check for ttsLevel. So, this trick will basically improve performance of certain "read" scenarios, but in order to improve performance for "write" scenarios, MS has to provide a fix.

Thursday, October 29, 2020

Marking via X++

X++ code for marking in D365FO (it is somewhat different from that in AX 2012):

    InventTransId         issueInventTransId   = 'x';
    InventTransId         receiptInventTransId = 'y';
 
    InventTransOriginId receiptInventTransOriginId = 
        InventTransOrigin::findByInventTransId(receiptInventTransId).RecId;
    InventTrans         receiptInventTrans         = 
        InventTrans::findByInventTransOrigin(receiptInventTransOriginId);
 
    InventTransOriginId issueInventTransOriginId = 
        InventTransOrigin::findByInventTransId(issueInventTransId).RecId;
    InventTrans         issueInventTrans       = 
        InventTrans::findByInventTransOrigin(issueInventTransOriginId);
 
    collection = TmpInventTransMark::markingCollection(
        InventTransOrigin::find(receiptInventTransOriginId),
        receiptInventTrans.inventDim(),
        receiptInventTrans.Qty);
 
    collection.insertCollectionToTmpTable(tmpInventTransMark);
 
    select firstonly tmpInventTransMark
        where tmpInventTransMark.InventTransOrigin == issueInventTrans.InventTransOrigin
           && tmpInventTransMark.InventDimId       == issueInventTrans.InventDimId;
 
    if (tmpInventTransMark.RecId != 0)
    {
        Qty qtyToMark = issueInventTrans.Qty;
 
        tmpInventTransMark.QtyMarkNow =  qtyToMark;
        tmpInventTransMark.QtyRemain  -= tmpInventTransMark.QtyMarkNow;
 
        mapUpdated = new Map(Types::Int64, Types::Record);
        mapUpdated.insert(tmpInventTransMark.RecId, tmpInventTransMark);
 
        TmpInventTransMark::updateTmpMark(
                receiptInventTransOriginId,
                receiptInventTrans.inventDim(),
                -qtyToMark,
                mapUpdated.pack());
    }

Wednesday, March 27, 2019

Common mistake, when creating Purchase Orders from X++

I would like to warn my fellow Axapta/AX/D365FO bloggers, that blind copy-pasting from each other seems innocent, but it fact may result in spreading bugs.

E.g., the following piece of code is very popular in "How to create Purchase Order from X++" posts.

numberSeq = NumberSeq::newGetNumFromCode(purchParameters::numRefPurchaseOrderId().NumberSequence,true);

Purchtable.PurchId = numberSeq.num();

The problem is numRefPurchaseOrderId returns the number sequence for purchase order confirmation number. For purchase order number, numRefPurchId should have been used. Using the wrong number sequence may result in duplicate key issues.

Thursday, December 6, 2018

Controlling grid line colors

Can see some folks have found the DisplayOptionInitialize event, still they are in doubt on how to control the displayOption settings based on the selected line.


Here is the solution:


X++:
 [FormDataSourceEventHandler(formDataSourceStr(Form1, SalesTable), FormDataSourceEventType::DisplayOptionInitialize)]
    public static void SalesTable_OnDisplayOptionInitialize(FormDataSource sender, FormDataSourceEventArgs e)
    {
        FormDataSourceDisplayOptionInitializeEventArgs args = e as FormDataSourceDisplayOptionInitializeEventArgs;
        RecId currentRecId = args.record().RecId;
        int rgbValue = 255 - (20 * (currentRecId mod 2));
        args.displayOption().backColor(WinAPI::RGB2int(rgbValue, rgbValue, rgbValue));
    }



Tuesday, November 6, 2018

Create LedgerDimension from main account and financial dimensions

The following code will (hopefully) keep generating valid LedgerDimension values, even after account structures or advanced rule structures are modified.

In the example below, there is an account structure with MainAccount-BusinessUnit-Department segments and an advanced rule with Project segment.

class TestLedgerDim
{        
    public static DimensionDynamicAccount generateLedgerDimension(
        MainAccountNum _mainAccountId,
        str _departmentId,
        str _businessUnit,
        str _projectId)
    {
        DimensionAttributeValueSetStorage dimensionAttributeValueSetStorage 
            = new DimensionAttributeValueSetStorage();

        void addDimensionAttributeValue(
            DimensionAttribute _dimensionAttribute, 
            str _dimValueStr)
        {
            DimensionAttributeValue dimensionAttributeValue;

            if (_dimValueStr != '')
            {
                dimensionAttributeValue = 
                    DimensionAttributeValue::findByDimensionAttributeAndValueNoError(
                        _dimensionAttribute,
                        _dimValueStr);
            }

            if (dimensionAttributeValue.RecId != 0)
            {
                dimensionAttributeValueSetStorage.addItem(dimensionAttributeValue);
            }
        }

        DimensionAttribute dimensionAttribute;

        while select dimensionAttribute
            where dimensionAttribute.ViewName == tableStr(DimAttributeOMDepartment)
               || dimensionAttribute.ViewName == tableStr(DimAttributeOMBusinessUnit)
               || dimensionAttribute.ViewName == tableStr(DimAttributeProjTable)
        {
            switch (dimensionAttribute.ViewName)
            {
                case tableStr(DimAttributeOMDepartment):
                    addDimensionAttributeValue(dimensionAttribute, _departmentId);
                    break;

                case tableStr(DimAttributeOMBusinessUnit):
                    addDimensionAttributeValue(dimensionAttribute, _businessUnit);
                    break;

                case tableStr(DimAttributeProjTable):
                    addDimensionAttributeValue(dimensionAttribute, _projectId);
                    break;
            }            
        }

        RecId defaultDimensionRecId = dimensionAttributeValueSetStorage.save();

        return LedgerDimensionFacade::serviceCreateLedgerDimension(
            LedgerDefaultAccountHelper::getDefaultAccountFromMainAccountRecId(
                MainAccount::findByMainAccountId(_mainAccountId).RecId),
            defaultDimensionRecId);
    }

    /// <summary>
    /// Runs the class with the specified arguments.
    /// </summary>
    /// <param name = "_args">The specified arguments.</param>
    public static void main(Args _args)
    {   
        // Prints 110110-001-022-000002
        info(
            DimensionAttributeValueCombination::find(
                TestLedgerDim::generateLedgerDimension(
                    '110110', 
                    '022', 
                    '001', 
                    '000002')).DisplayValue);
    }

}

Tuesday, November 29, 2016