Data upgrade from Ax 2012 to Dynamics 365 Finance and Operations. Migrate inventory dimensions.

Recently one of the customers asked me if a data migration from Ax 2012 to the latest finance and operations version is possible. The Ax 2012 solution has additional inventory dimensions that migrated to the D365 from a code perspective. While investigating the issue I faced some issues and I would like to share my experience regarding inventory dimensions data upgrade. I hope it will save your time.

The documentation regarding the data upgrade process is pretty clear and there is a separate page related to the troubleshooting upgrade scripts experience.

During the data migration in development environment, I faced the same error as mentioned on the troubleshooting page:

The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.INVENTDIM' and the index name 'I_698SHA1HASHIDX'. The duplicate key value is (5637144576, USMF, B92BAF2504FD67FC15A56F2DFACE09127715B54B). The statement has been terminated. CREATE UNIQUE INDEX I_698SHA1HASHIDX ON DBO.INVENTDIM(PARTITION,DATAAREAID,SHA1HASHHEX) WITH (MAXDOP = 1)

It was unexpected since a special inventory dimension data migration script was developed. When I run the recommended query to verify the script, this is what appears:

SELECT PARTITION, DATAAREAID, SHA1HASHHEX, COUNT(*)
FROM INVENTDIM
GROUP BY PARTITION, DATAAREAID, SHA1HASHHEX
HAVING COUNT(*) > 1

The query returned many records. In fact, it means that the SHA1HASHHEX field values were not generated as unique. In my opinion, there can be two reasons except those described in the documentation: The values of InventDimension* fields are not taken into consideration with the ReleaseUpdateDB72_Invent::updateSHA1HashHexInInventDim method or the InventDimension* fields are empty.

In my case, the reason was that the appropriate configuration keys InventDimension* are disabled in the system by default. Even if I violated the system and copied the data from Ax 2012 inventory dimensions fields to the appropriate InventDimension* fields via direct SQL, the script still looks like this:

[
  UpgradeScriptDescription("Script description"),
  UpgradeScriptStage(ReleaseUpdateScriptStage::PreSync),
  UpgradeScriptType(ReleaseUpdateScriptType::StandardScript),
  UpgradeScriptTable(tableStr(InventDim), false, true, true, false)
]
public void updateInventDimensionField()
{
    FieldName     field2012Name  = 'YOURDIMENSIONFIELD';
  FieldName     field365Name   = 'INVENTDIMENSION1';
  SysDictTable  inventDimDT    = SysDictTable::newTableId(tableNum(InventDim));
  TableName     inventDimName  = inventDimDT.name(DbBackend::Sql);

  str sqlStatement = strFmt(@"

                UPDATE [dbo].[%1]
                SET [dbo].[%1].[%2] = [dbo].[%1].[%3]
                WHERE [dbo].[%1].[%3] <> ''",
                inventDimName,
                field365Name,
                field2012Name);
   
  ReleaseUpdateDB::statementExeUpdate(sqlStatement);
}

The system did not use its values with the generate hash values method and the above-mentioned synchronization issue appeared.

In order to fix this issue, the required configuration keys should be activated during the data upgrade. In order to perform it, the following method can be used:

[

  UpgradeScriptDescription("Enable inventory dimension configuration keys"),
  UpgradeScriptStage(ReleaseUpdateScriptStage::PreSync),
  UpgradeScriptType(ReleaseUpdateScriptType::StartScript),
    Microsoft.Dynamics.BusinessPlatform.SharedTypes.InternalUseOnlyAttribute
]
public void enablePreSyncInventDimensionConfigurationKeys()
{
  ConfigurationKeySet keySet = new ConfigurationKeySet();
  SysGlobalCache      cache = appl.globalCache();

  keySet.loadSystemSetup();

  keySet.enabled(configurationKeyNum(InventDimension1), true);
  keySet.enabled(configurationKeyNum(InventDimension2), true);

  SysDictConfigurationKey::save(keySet.pack());

  SysSecurity::reload(true, true, true, false, false);

}

When the above- mentioned method is used and a data transfer between Ax 2012 and D365 InventDim fields is completed (It should be done at the PreSync step, otherwise the Ax 2012 InventDim custom fields will be deleted at the DbSync step), the ReleaseUpdateDB72_Invent::updateSHA1HashHexInInventDim method should generate hash values correctly and the synchronization issue should be resolved.

Additionally, I recommend updating other tables that contain InventDim field ids from Ax2012 version since InventDimension* fields in D365 have other field id values. I believe every system may have various setups and customizations. Therefore, from my point of view, it make sense to develop scripts for updating setups and the field id values of the InventDim table in the tables listed below:

  • WHSReservationHierarchyElement
  • EcoResStorageDimensionGroupFldSetup
  • EcoResTrackingDimensionGroupFldSetup   
  • EcoResProductDimensionGroupFldSetup 
The correct values in the InventDim table and in the tables listed above, as well as the enabled configuration keys, should help to avoid problems with data WHS* tables within the upgrade process.

Finally, if you see any of these errors after the data migration:

DECLARE @AVAILPHYSICAL numeric(32, 6) DECLARE @AVAILORDERED numeric(32, 6); EXECUTE WHSOnHandWithInventDim @DATAAREAID = N'USMF', @PARTITION = 5637144576, @ITEMID = N'ITEMIDXXX', @LEVEL = 6, @UPPERLEVELOFMINIMUMCALCULATION = 0, @INVENTSITEID = N'1', @INVENTSITEIDLEVEL = 1, @INVENTLOCATIONID = N'16', @INVENTLOCATIONIDLEVEL = 2, @INVENTSTATUSIDLEVEL = 3, @INVENTBATCHIDLEVEL = 4, @INVENTDIMENSION1 = N'DIMENSIONVALUE', @INVENTDIMENSION1LEVEL = 5, @WMSLOCATIONID = N'12', @WMSLOCATIONIDLEVEL = 6, @AVAILPHYSICAL = @AVAILPHYSICAL output, @AVAILORDERED = @AVAILORDERED output ; SELECT @AVAILPHYSICAL as [AVAILPHYSICAL], @AVAILORDERED as [AVAILORDERED];

Microsoft][ODBC Driver 17 for SQL Server][SQL Server]@INVENTDIMENSION1 is not a parameter for procedure WHSOnHandWithInventDim.

It means the WHSOnHandWithInventDim and WHSOnHandWithDelta procedures were not adjusted to the current dimension configuration setups. It could happen if at the final database synchronization step during the data migration process the required configuration keys were not enabled. In order to fix this error, you can run the code via a job:

WHSOnHandSPHelper::syncDBStoredProcedures();

It should align the mentioned stored procedures to the current activated InventDimension* configuration keys and the errors should no longer be.

D365 Finance and Operations. Get path for the menu item via code.

In one of my previous posts, I have shared an idea of how a new Dynamics 365 SCM and Finance API can be used to get metadata of Dynamics 365 AOT elements. 

Learning a path to a menu item from a user interface perspective could come in handy.

So I have created the method that I would like to share:

public static str getMenuItemPath(str _menuItemName)
{
   MenuFunction mf = new MenuFunction(_menuItemName, MenuItemType::Display);
   str          menuLabel;
   str          ret;

   boolean iterateMenus(SysDictMenu _menu)

   {
      SysMenuEnumerator       menuEnum = _menu.getEnumerator();
      SysDictMenu             subMenu;
      boolean                 found;

      while (menuEnum.moveNext())

      {
         subMenu = menuEnum.current();
         if (subMenu.isMenu() || subMenu.isMenuReference())
         {
            found = iterateMenus(subMenu);

            if (found) // If found, just climb back up the stack

            {
               menuLabel = strFmt('%1/%2', subMenu.label(), menuLabel);

               return found;

            }
         }
         else
         {

            if (subMenu.isMenuItem()

             && subMenu.isValid()
             && subMenu.isVisible()
             && subMenu.menuItem().name() == mf.name()
             && subMenu.menuItem().type() == mf.type())
            {
               menuLabel = subMenu.label();

               return true;

            }
         }
      }

      return false;

   }

   If (iterateMenus(SysDictMenu::newMainMenu()))

   {
      ret = strfmt('%1', menuLabel);
   }

   return ret;

}

You can find a couple of examples of using the method presented above, below :

1. info(strFmt('%1', Class::getMenuItemPath('SalesTableListPage')));

It will return the result: Accounts receivable/Orders/All sales orders


2. info(strFmt('%1', Class::getMenuItemPath(menuItemDisplayStr(InventLocations))));

It will return the result: Inventory management/Setup/Inventory breakdown/Warehouses




Installation of Microsoft Dynamics 365 for Finance and Operations software deployable package on a development environment (one that is not connected to LCS)

As you may know the development environment can be hosted on LCS or deployed as virtual machine on your laptop. In my case, I use the laptop in order to host my development virtual machine.  Since Microsoft releases about 10 updates every year I install them periodically in my dev box.

The installation process of the deployable package is described in the standard documentation. All key terms and definitions are perfectly explained there. 

I would like to share my experience of the installation process for the standard “all in one” virtual machine. I hope it can be useful and save your time. 

So let’s start.

1. Download the software deployable package from LCS on the virtual machine (VM) in a non-user folder.

2. After the zip file is downloaded, right-click it, and then select Properties. Then, in the "Properties" dialog box, on the "General" tab, select "Unblock" to unlock the files. (https://community.dynamics.com/ax/f/33/t/244550)

3. Extract the files to a folder. Here we will use C:\Temp\PU20\AXPlatformUpdate folder as an example.

4. Open a Command Prompt window “as Administrator” mode and change the folder with the command: cd C:\Temp\PU20\AXPlatformUpdate

5. Execute the command: AXUpdateInstaller.exe list It will show the list of installed components:


All listed components should be added to the DefaultTopologyData.xml file. This file is placed in the same C:\Temp\PU20\AXPlatformUpdate folder. If we open this file with Notepad, it must look like this:


It is needed to add the list of the components to the DefaultTopologyData.xml file.

6. Generate the runbook file that will provide step-by-step instructions for updating the VM. In our case the command for generating the runbook file will be: 
AXUpdateInstaller.exe generate 
-runbookid="AZH81-runbook" 
-topologyfile="DefaultTopologyData.xml" 
-servicemodelfile="DefaultServiceModelData.xml" 
-runbookfile="AZH81-runbook.xml"


runbookID – A parameter specified by the developer, it applies the deployable package.
topologyFile – The path of the DefaultTopologyData.xml file.
serviceModelFile – The path of the DefaultServiceModelData.xml file.
runbookFile – The name of the runbook file to generate (for example, AZH81-runbook.xml).

After executing the command the file AZH81-runbook.xml will be created in the C:\Temp\PU20\AXPlatformUpdate folder. If we open the created file, it will look like:


7. Verify that everyone has full permissions for the C:\Temp\PU20\AXPlatformUpdate folder and remove the “Read only” property for the mentioned folder.

8. Import the runbook by running the following command: 
AXUpdateInstaller.exe import -runbookfile="AZH81-runbook.xml"

9. Run the runbook by running the following command:
AXUpdateInstaller.exe execute -runbookid=AZH81-runbook
While running the process, messages as on the screenshot below should appear:


Note: If step in the runbook fails, you can rerun it by running the following command:
AXUpdateInstaller.exe execute -runbookid=“AZH81-runbook” -rerunstep=6
The example above is provided for the case if step №6 has failed.

My troubleshooting experience

Database synchronization fails

Executing step: 24

GlobalUpdate script for service model: AOSService on machine: localhost

Sync AX database
The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: System.Management.Automation.RuntimeException: An exception of type System.Net.WebException occurred when making an http request to: http://127.0.0.1/ReportServer. Refer to the log file for more details.
The step failed
The step: if 24 failed, you can use rerun step command to debug the step explicitly
at Microsoft.Dynamics.AX.AXUpdateInstallerBase.RunbookExecutor.ExecuteRunbookStepList(RunbookData runbookData, String updatePackageFilePath, Boolean silent, String stepID, ExecuteStepMode executeStepMode, Boolean versionCheck, Parameters parameters)
at Microsoft.Dynamics.AX.AXUpdateInstallerBase.AXUpdateInstallerBase.execute(String runbookID, Boolean silent, String updatePackageFilePath, IRunbookExecutor runbookExecutor, Boolean versionCheck, Parameters param)
at Microsoft.Dynamics.AX.AXUpdateInstaller.Program.InstallUpdate(String[] args)
at Microsoft.Dynamics.AX.AXUpdateInstaller.Program.Main(String[] args)
OR 
Executing step: 24
GlobalUpdate script for service model: AOSService on machine: localhost
Sync AX database
The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: System.Management.Automation.RuntimeException: An exception of type System.Net.WebException occurred when making an http request to: http://127.0.0.1:80/ReportServer. Refer to the log file for more details.
The step failed

The very first time this appeared in PU 30 and it is still happening. To fix this issue AutoDeployReportAndSyncDB.ps1 file needs modification. In our example it will be in: 
C:\Temp\PU20\AXPlatformUpdate\AOSService\Scripts\AutoDeployReportAndSyncDB.ps1

After the  #db sync add the command:  invoke-Expression "net start reportserver"

#db sync 
invoke-Expression "net start reportserver"

Save the changes and rerun the failed step by running the following command:  
AXUpdateInstaller.exe execute -runbookid=“AZH81-runbook” -rerunstep=24

Timeout error

Executing step: 24
GlobalUpdate script for service model: AOSService on machine: localhost
Sync AX database
The step execution time exceed the timeout value specified: 120min
The step failed.

It might happen if your local hosted virtual machine does not have enough performance. It can happen on any step and be fixed in the same way. Increasing the timeout value for the appropriate step is advisable.

In order to resolve this particular issue you need to modify the runbook file. In our example it is AZH81-runbook.xml file from folder C:\Temp\PU20\AXPlatformUpdate. It is needed to find the following section of the file and increase the TimeoutValue value:

<GlobalUpdateScript>
          <FileName>AutoDeployReportAndSyncDB.ps1</FileName>
          <Automated>true</Automated>
          <Description>Sync AX database</Description>
          <RetryCount>0</RetryCount>
          <TimeoutValue>120</TimeoutValue>
          <InvokeWithPowershellProcess>false</InvokeWithPowershellProcess>
          <DynamicStepDefinition />
  </GlobalUpdateScript>

For example, we can set 720 minutes:

<GlobalUpdateScript>
          <FileName>AutoDeployReportAndSyncDB.ps1</FileName>
          <Automated>true</Automated>
          <Description>Sync AX database</Description>
          <RetryCount>0</RetryCount>
          <TimeoutValue>720</TimeoutValue>
          <InvokeWithPowershellProcess>false</InvokeWithPowershellProcess>
          <DynamicStepDefinition />
  </GlobalUpdateScript>

Save the changes and rerun the failed step by running the following command:
AXUpdateInstaller.exe execute -runbookid=“AZH81-runbook” -rerunstep=24

If rerun of the process does not work, you need to import the runbook file by running the following command: AXUpdateInstaller.exe import -runbookfile="AZH81-runbook.xml"

And run the process from the very beginning by running the following command: AXUpdateInstaller.exe execute -runbookid=AZH81-runbook 

If it is not possible to run the process from the very beginning , you can restart the VM and run the process from the very beginning by running the following command: 
AXUpdateInstaller.exe execute -runbookid=AZH81-runbook

Batch Management Service error

If the Batch Management Service does not work and you can’t start it and you see the errors in Event Viewer as on screenshots below:

In addition, you get the error like those mentioned below:

Executing step: 63

Start script for service model: AOSService on machine: localhost
Start AOS service and Batch service
SUCCESS: The scheduled task "DynamicsServicingCopyStaging" has successfully been created.
Error during AOS start: Failed to start service 'Microsoft Dynamics 365 Unified Operations: Batch Management Service (DynamicsAxBatch)'. [Log: C:\Temp\PU20\AXPlatformUpdate\RunbookWorkingFolder\AZH81-runbook\localhost\AOSService\63\Log\AutoStartAOS.log]
The step failed
The step: if 63 failed, you can use rerunstep command to debug the step explicitly at Microsoft.Dynamics.AX.AXUpdateInstallerBase.RunbookExecutor.ExecuteRunbookStepList(RunbookData runbookData, String updatePackageFilePath, Boolean silent, String stepID, ExecuteStepMode executeStepMode, Boolean versionCheck, Parameters parameters)
at Microsoft.Dynamics.AX.AXUpdateInstaller.Program.InstallUpdate(String[] args)
at Microsoft.Dynamics.AX.AXUpdateInstaller.Program.Main(String[] args)

It means that your environment is in a maintenance mode. You should turn off the maintenance mode. (https://docs.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/sysadmin/maintenance-mode)
Then you can start the Batch Management Service and continue the updating process by running the following command: 
AXUpdateInstaller.exe execute -runbookid=“AZH81-runbook” -rerunstep=63












User-based service protection API limits in Microsoft Dynamics 365 Finance and Operations

Last month Microsoft announced user-based service protection API limits in Microsoft Dynamics 365 Finance and Operations. This information can be crucial for a project if there are many various integration flows implemented.

These limits are designed to preserve service health in the event of client applications with extraordinary demands on server resources. Additionally, these limits are designed to throttle incoming API requests when there are sudden bursts of traffic or concurrent long-running requests against the server.

The limits are designed to affect client applications that perform extraordinary API requests only, and protect against unexpected surges. Normally regular users of interactive clients and standard data integrations shouldn’t be affected. However, implemented OData and custom service integrations may require changes to handle the API limits. Requests that exceed the limits will return a 429 response with a Retry-After interval. So it may be required to make updates to optimize integrations, maximizing throughput.

The limits are in addition to the resource-based service protection API limits that have been in place for Finance and Operations apps environments since version 10.0.19 (which protects the environment from aggregated user demand exhausting environment resources). The user-based limits define specific API usage thresholds for OData and custom API (service) endpoints to prevent individual users or integrations from causing outages or degrading environment performance.

For now, the service protection API limits don't apply to all Microsoft services. The following services are currently exempt from them:

At this moment, according to the published documentation the new user-based service protection API limits will be available to enable in Finance and Operations apps environments with version 10.0.28 (PU53). In version 10.0.29 (PU54), with 2022 release wave 2, the API limits will be enabled by default in all environments. The limits will be optional, allowing environment administrators to temporarily disable them should additional time be needed to optimize integrations. In version 10.0.32 (PU56), with 2023 release wave 1, the API limits will be mandatory and the option to disable API limits will no longer be provided. So there is about one year to prepare for the new API limits.

The official documentation about the new feature can be found:

Note: The documentation is talking about "User-based" and not integration-based (app-id-based) but it seems "User-based" throttling is referring to the service principal, which is where the requests are being measured.

Also, here is a TechTalk video I find useful. This video can provide guidance and can answer some questions related to service protection API limits. 

In addition, I was able to find some topics related to the API limits which I found interesting, on the Internet. I wrote down the interesting points that I would like to share:

  • If a generic user is used to run recurring batch jobs there is no option to opt this user out of the user-based service protection API limits so far.
  • OData requests with $batch (like exchange rates) will be subject to the same throttling limits. It may be needed to adjust the number of records included in the batch to not exceed any of the three usage limits.
  • An example of "execution time" for throttling:

There are 6000 oData calls per 5 minutes and each call takes 1 second to execute. 

Because of "Execution time" limits (20 minutes (1,200 seconds)) in this scenario number of calls  will be limited to 1200 since the limit is on the cumulative execution time of the API requests for that user/service principal within the 5-minute sliding window. So the number of API requests that can be made, will be limited by how compute-intensive the requests are.


My Performance Resource Page for Microsoft Dynamics AX 2012/Microsoft Dynamics 365 Finance and Operations

A performance issue can have many aspects, so in my opinion there is no universal advice. Below you can find the links I’ve collected, that I use for different cases as a starting point. 

If you face a system performance issue and you are not sure how to deal with it, I would recommend watching the TechTalk Performance Testing in Microsoft Dynamics 365 Series. In my opinion, it can be a good starting point in order to understand the process.

Also, I would recommend a training as another starting point so that you can have a knowledge about the tools: Work with performance and monitoring tools in Finance and Operations apps 

If you need to work with LCS environments:

In some cases, a data clean-up operation can improve the system’s performance. For example, the “On-hand entries cleanup" operation can improve inventory operations performance. From the list, you can select the appropriate operations and repeat them regularly in order to keep the database size under control.

If the issue can be reproduced in the environment with installed Trace Parser, the links below can be useful as well:

The following tips can help to improve the performance of a DMF project:

If you would like to have a performance audit, Denis’ article will be really helpful:

If you use AX 2012 version the following links can be helpful:

Although the tips are for Ax 2012, some ideas from this page can be relevant for Dynamics 365 Finance and Supply Chain Management.

Dynamics 365 SCM and Finance API for getting security metadata

Dynamics 365 SCM and Finance API for getting security metadata 

Microsoft Dynamics 365 for Finance and Operations offers a new API for getting metadata of AX elements such as tables, form extensions, models, and so on. In my case, this API has been used for creating a security report and I would like to share my experience.

The initial idea was to understand what objects are available for users and what access level they have. I’m going to share a part of the code just to give an example. In a similar way, it should be possible to expand the given example to other security objects.

The security model description of the Dynamics 365 finance and operations is well explained in the standard documentation:

I will not repeat its description here. Our goal was to develop the report that can provide information about forms, tables and other objects that user has access to. 

As an example we will consider a user’s role. This role has the following structure:

Privilege details:

Duty details:

In order to get the user access information can be used the code below: 
Note: The processing of the sub role root will be not covered in the provided code example.

using Microsoft.Dynamics.AX.Metadata.Storage;

using Microsoft.Dynamics.AX.Metadata.Providers;
using Microsoft.Dynamics.AX.Metadata.MetaModel;
using Microsoft.Dynamics.ApplicationPlatform.Environment;
using Microsoft.Dynamics.AX.Security.Management.Domain;
using Microsoft.Dynamics.AX.Security.Management;
using Microsoft.Dynamics.AX.Metadata.Core.MetaModel;

class TestClass

{
    void run(str _aotName = 'D365TestRole')
    {
        Microsoft.Dynamics.AX.Metadata.Core.MetaModel.UserLicenseType   maintainLicense;
        Microsoft.Dynamics.AX.Metadata.Core.MetaModel.UserLicenseType   viewLicense;
        MenuFunction            menuFunction;
        AxSecurityRole          roleSec;
        SecurityRole            securityRole;
        Privilege               privilege;
        var                     environment  = EnvironmentFactory::GetApplicationEnvironment();
        var                     packagesLocalDirectory  = environment.Aos.PackageDirectory;

        IMetadataProvider     metadataProvider
                    = new MetadataProviderFactory().CreateDiskProvider(packagesLocalDirectory);

        IMetaSecurityPrivilegeProvider privilegeProvider  = metadataProvider.SecurityPrivileges;
        IMetaSecurityDutyProvider      dutyProvider       = metadataProvider.SecurityDuties;
        IMetaSecurityRoleProvider      roleProvider       = metadataProvider.SecurityRoles;

        select * from securityRole

            where securityRole.AotName == _aotName;

        if (securityRole)

        {
            roleSec 
              = Microsoft.Dynamics.Ax.Xpp.MetadataSupport::GetSecurityRole(securityRole.AotName);

            info("--Role privileges--");

            AxSecurityPrivilegeReference privilegesRef;

            AxSecurityPrivilege          axSecPrivilege;

            var privilegesEnum = roleSec.Privileges.GetEnumerator();

            while (privilegesEnum.MoveNext())
            {
                privilegesRef   = privilegesEnum.get_current();
                axSecPrivilege  = privilegeProvider.Read(privilegesRef.Name);

                this.expandPrivilege(axSecPrivilege);

            }

            info("--Role Duties--");

            Duty                            duty;

            AxSecurityDutyReference         dutyRef;
            AxSecurityDuty                  axSecDuty;

            var dutiesEnum = roleSec.Duties.GetEnumerator();

            while (dutiesEnum.MoveNext())
            {
                dutyRef     = dutiesEnum.get_current();
                axSecDuty   = dutyProvider.Read(dutyRef.Name);

                info(strFmt('Duty %1 Label %2',

                            axSecDuty.Name,
                            SysLabel::labelId2String2(axSecDuty.Label, currentUserLanguage())));

                var dutyPrivileges          = axSecDuty.Privileges;

                var privilegesEnumerator    = dutyPrivileges.GetEnumerator();

                while (privilegesEnumerator.MoveNext())

                {
                    privilegesRef   = privilegesEnumerator.get_current();
                    axSecPrivilege  = privilegeProvider.Read(privilegesRef.Name);

                    this.expandPrivilege(axSecPrivilege);

                }
            }

            info("--Role Sub Roles--");

            AxSecurityRole          subRole;

            var subRolesEnum = roleSec.SubRoles.GetEnumerator();

            while (subRolesEnum.MoveNext())
            {
                AxSecurityRoleReference roleRef = subRolesEnum.get_current();

                subRole = roleProvider.Read(roleRef.Name);

                info(strFmt('SubRole %1 Label %2',

                            subRole.Name,
                            SysLabel::labelId2String2(subRole.Label, currentUserLanguage())));

                //expand sub-roles

                . . . . . . . . . .
                . . . . . . . . . .
            }

            info("--Role Permissions--");

            var accessPermissionsEnum = roleSec.DirectAccessPermissions.GetEnumerator();

            while (accessPermissionsEnum.MoveNext())
            {
                AxSecurityDataEntityReference roleReference; 
                roleReference = accessPermissionsEnum.get_current();

                AxTable         table;

                table = Microsoft.Dynamics.Ax.Xpp.MetadataSupport::GetTable(roleReference.Name);         
                if (table)
                {                 
                    var fieldsRefEnum = RoleReference.Fields.GetEnumerator();

                    while (fieldsRefEnum.MoveNext())

                    {
                        AxSecurityDataEntityFieldReference fieldsRef; 
                        fieldsRef = fieldsRefEnum.get_current();

                        AccessGrant     grant;

                        grant = fieldsRef.Grant;

                        str grantRead   = enum2Str(grant.Read);

                        str grantUpdate = enum2Str(grant.Update);
                        str grantCreate = enum2Str(grant.Create);
                        str grantDelete = enum2Str(grant.Delete);
                        str grantCorrect= enum2Str(grant.Correct);

                        info(strFmt('Table %1 Field %2', table.Name, fieldsRef.Name));

                        Info(strFmt('Read %1 Update %2 Create %3 Delete %4 Correct %5',

                                    grantRead,
                                    grantUpdate,
                                    grantCreate,
                                    grantDelete,
                                    grantCorrect));
                    }
                }
            }     
        }
    }

    void expandPrivilege(AxSecurityPrivilege     _axSecPrivilege)

    {
       Microsoft.Dynamics.AX.Metadata.Core.MetaModel.UserLicenseType   maintainLicense;
       Microsoft.Dynamics.AX.Metadata.Core.MetaModel.UserLicenseType   viewLicense;

       AxSecurityEntryPointReference   entryPoint;

       MenuFunction                    menuFunction;
       AccessGrant                     grant;   

       info(strFmt("Privilege %1", _axSecPrivilege.Name));

       info("--Privilege EntryPoints--");

       var privilegeEntryPoints = _axSecPrivilege.EntryPoints;

       System.Collections.IEnumerator  dotNetEnumerator = privilegeEntryPoints.GetEnumerator();

       while (dotNetEnumerator.MoveNext())
       {        
          entryPoint = dotNetEnumerator.get_Current();        

          switch (entryPoint.ObjectType)

          {

          case Microsoft.Dynamics.AX.Metadata.Core.MetaModel.EntryPointType::MenuItemDisplay :

          menuFunction 
          new MenuFunction(entryPoint.ObjectName,
          Microsoft.Dynamics.AX.Metadata.Core.MetaModel.MenuItemType::Display);
          break;

          case Microsoft.Dynamics.AX.Metadata.Core.MetaModel.EntryPointType::MenuItemOutput :

          menuFunction 
          = new MenuFunction(entryPoint.ObjectName,  
                             Microsoft.Dynamics.AX.Metadata.Core.MetaModel.MenuItemType::Output);
          break;

          case Microsoft.Dynamics.AX.Metadata.Core.MetaModel.EntryPointType::MenuItemAction :

          menuFunction 
          = new MenuFunction(entryPoint.ObjectName, 
                             Microsoft.Dynamics.AX.Metadata.Core.MetaModel.MenuItemType::Action);
          break;

           //other types can be added here

           . . . . . . . . . .
           . . . . . . . . . .
      }

      if (menuFunction)

      {

         maintainLicense = menuFunction.maintainUserLicense();

         viewLicense     = menuFunction.viewUserLicense();

         Info(strFmt('Privilege %1 Object %2 MainLicense %3 ViewLicense %4',

                      _axSecPrivilege.Name,
                      entryPoint.ObjectName,
                      enum2Str(maintainLicense),
                      enum2Str(viewLicense)));
      }

      grant = entryPoint.Grant;           

      str grantRead   = enum2Str(grant.Read);

      str grantUpdate = enum2Str(grant.Update);
      str grantCreate = enum2Str(grant.Create);
      str grantDelete = enum2Str(grant.Delete);
      str grantCorrect= enum2Str(grant.Correct);
      str grantInvoke = enum2Str(grant.Invoke);       

      Info(strFmt('ObjectType %1 Name %2 ChildName %3',

                  enum2Str(entryPoint.ObjectType),
                  entryPoint.ObjectName,
                  entryPoint.ObjectChildName)); 

      Info(strFmt('Read %1 Update %2 Create %3 Delete %4 Correct %5',

                   grantRead,
                   grantUpdate,
                   grantCreate,
                   grantDelete,
                   grantCorrect));
    }

    info("--Privilege DataEntityPermissions--");

    var dataEntityPermissions = _axSecPrivilege.DataEntityPermissions;   

    System.Collections.IEnumerator  dataEntityEnumerator = dataEntityPermissions.GetEnumerator();

    while (dataEntityEnumerator.MoveNext())
    {
        AxSecurityDataEntityPermission entityPermision = dataEntityEnumerator.get_current();     
        AccessGrant     grantEntity;

        grantEntity = entityPermision.Grant;

        str grantRead   = enum2Str(grantEntity.Read);

        str grantUpdate = enum2Str(grantEntity.Update);
        str grantCreate = enum2Str(grantEntity.Create);
        str grantDelete = enum2Str(grantEntity.Delete);
        str grantCorrect= enum2Str(grantEntity.Correct);

        info(strFmt('DataEntity %1', entityPermision.Name));

        Info(strFmt('Read %1 Update %2 Create %3 Delete %4 Correct %5',

                     grantRead,
                     grantUpdate,
                     grantCreate,
                     grantDelete,
                     grantCorrect));         
    }

    //Permissions, entry points and so on . . .

    . . . . . . . . . . . . . .
    . . . . . . . . . . . . . .
 }

public static void main(Args _args)

{
   TestClass workClass = new TestClass();    

   workClass.run();

}

}

Note: It is an example. It covers not all possible security cases. It can be used as a starting point for development.

The result of this code for the D365TestRole will be like this:

--Role privileges--
    Privilege AbbreviationsEntityView
    --Privilege EntryPoints--
    --Privilege DataEntityPermissions--
           DataEntity AbbreviationsEntity
               Read: Allow Update: Unset Create: Unset Delete: Unset Correct: Unset
--Role Duties--
    Duty AccountingDistCustFreeInvoiceMaintain Label Maintain accounting distribution for customer      free text invoice duty
        Privilege AccountingDistCustFreeInvoiceDocumentMaintain
        --Privilege EntryPoints--
            Privilege AccountingDistCustFreeInvoiceDocumentMaintain 
            Object AccountingDistributionsDocumentView 
            MainLicense Universal ViewLicense Universal
            ObjectType MenuItemDisplay Name AccountingDistributionsDocumentView 
                Read: Allow Update: Allow Create: Allow Delete: Allow Correct: Allow
        --Privilege DataEntityPermissions--
--Role Sub Roles--
    SubRole AnonymousApplicant Label Applicant anonymous (external)
--Role Permissions--
    Table ActualWorkItemEntry Field Currency
        Read:Allow Update: Allow Create: Allow Delete: Unset Correct: Allow
    Table ActualWorkItemEntry Field Customer
        Read: Allow Update: Unset Create: Unset Delete: Unset Correct: Unset

How to run batch tasks using the SysOperation framework

Overview As you may know, the system has batch tasks functionality . It can be used to create a chain of operations if you want to set an or...