Tips & Tricks
VM shutdown
- To minimize the chance of your database going into an inconsistent state, please ensure you shut down your VM from within the VM first before shutting down your computer. This only applies to scenarios where the VM is hosted on your computer.
Useful Links
- Azure https://portal.azure.com/
- Postman https://www.postman.co/
- For viewing LCS https://sa.lcs.dynamics.com/
- For getting Public IP with Cloudflare https://speed.cloudflare.com/ OR https://radar.cloudflare.com/ip
- For getting Public IP with CMD OR PowerShell
nslookup myip.opendns.com. resolver1.opendns.com - For viewing a table use https://{d365ffo-url}/?cmp={company}&mi=SysTableBrowser&tableName=placeholder
- For editing a table use https://{d365ffo-url}/?cmp={company}&mi=DEVSysTableBrowser&TableName=placeholder
- For executing a runnable class & SysOperation use https://{d365ffo-url}/?cmp={company}&mi=SysClassRunner&cls=placeholder
- For viewing system errors use https://{d365ffo-url}/?cmp={company}&mi=DEVCSXppCallStackTable
- For executing a sql statement use https://{d365ffo-url}/?cmp={company}&mi=DEVSQLQueryExecute
- Finance & Operations
- Setup default model in VS**
C:/Users/%USERNAME%/Documents/Visual Studio Dynamics 365/DynamicsDevConfig.xml/<DefaultModelForNewProjects>{ModelPrefix}</DefaultModelForNewProjects>
Common Commands
-
Install Powershell D365 tools
Install-Module -Name d365fo.tools
-
Run Db sync on one module
- Run Powershell command
Invoke-D365DbSyncModule -Module {ModelPrefix}ORInvoke-D365DbSync
- Run Powershell command
-
RDP session getting disconnected continuously or hanging
- Login from another server - e.g., remote into the AX dev server and then remote into your D365 server from there
-
Server continuously restarting/Windows license expired
- Run CMD command
slmgr /rearm
- Run CMD command
-
Getting a error on a DLL while doing a get latest; run the script:
NET STOP "MR2012ProcessService" NET STOP "DynamicsAxBatch" NET STOP "Microsoft.Dynamics.AX.Framework.Tools.DMF.SSISHelperService.exe" NET STOP "W3SVC" PAUSE NET START "MR2012ProcessService" NET START "DynamicsAxBatch" NET START "Microsoft.Dynamics.AX.Framework.Tools.DMF.SSISHelperService.exe" NET START "W3SVC" PAUSE iisreset
-
Apply the platform update package to your development environment:
- Install the deployable package
- Software deployable package files
# Change the working directory to: cd /d D:\Downloads\FinanceAndOperations_10.0.0000.00_Application # Rerun Step ID 1: AXUpdateInstaller.exe execute -runbookid=OneBoxDev -rerunstep=1 # Set step complete, for Step ID 1: AXUpdateInstaller.exe execute -runbookid=OneBoxDev -setstepcomplete=1 # This will run the command for values 1 to 13, all in one line: for /L %i in (1,1,13) do AXUpdateInstaller.exe execute -runbookid=OneBoxDev -setstepcomplete=%i
-
Access Azure File Storage with File Manager:
Username: {StorageAccount}.file.core.windows.net\{StorageAccount} Path: \\{StorageAccount}.file.core.windows.net\{folder} Password: {Accesskey} -
Update a OneBox DevTest environment to connect to the UAT database
- On your Services drive, go to the
AoSService\WebRootdirectory. (Typically, the Services drive is drive J or K.) Find the file that is named web.config, and make a backup of it. Then open theweb.configfile in Notepad or another editor, and find the following configurations: - https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/database/dbmovement-scenario-debugdiag
<add key="DataAccess.Database" value="<example_axdb_fromAzure>" /> <add key="DataAccess.DbServer" value="<example_axdb_server.database.windows.net>" /> <add key="DataAccess.SqlPwd" value="<axdbadmin_password_from_LCS>" /> <add key="DataAccess.SqlUser" value="axdbadmin" /> <add key="DataAccess.AxAdminSqlPwd" value="<axdbadmin_password_from_LCS>" /> <add key="DataAccess.AxAdminSqlUser" value="axdbadmin" />
- On your Services drive, go to the
-
CMD - Execute SQL File
for %%G in (*.sql) do sqlcmd /S <server> /d <database> -E -i"%%G" pause for %%G in (*.sql) do sqlcmd /S <server> /d <database> -U <username> -P <password> -i"%%G" pause
-
To change the admin user for a UAT environment in Dynamics 365 Finance & Operations (D365 F&O), you’ll need to update the environment administrator in Lifecycle Services (LCS). Here's how to do it:
-
Steps to Change the Environment Admin in D365 UAT
-
For Tier 2+ Environments (like UAT)
- Log into LCS and go to your project.
- Open the Environment Details page for the UAT environment.
- Click on Maintain > Update Environment Admin.
- In the dialog box, select a new user who is either:
- A Project Owner, or
- An Environment Admin in the same LCS project.
- Click Save. This change will cause downtime in the target environment, so plan accordingly.
-
For Tier 1 Environments (Dev Boxes)
- If you're working with a Tier 1 dev box, the process involves PowerShell:
Install-Module -Name D365FO.tools Import-Module -Name D365FO.tools Set-D365Admin -AdminSignInName "admin@yourdomain.com"
- You’ll need RDP access to the environment and PowerShell ISE is recommended for smoother execution.
-
-
Tips
- The new admin must belong to the same Azure AD tenant as the environment.
- If you're changing domains, you’ll need a new ISV license file that reflects the updated domain.
- Always use a generic admin account (e.g.,
d365admin@yourdomain.com) to avoid issues when employees leave the organization.
-
You can find the official Microsoft guide on updating the environment administrator.
-
Various Documentation
-
General Information
-
{company}/Sales and marketing/Common/Contacts/All contacts -
{company}/Sales ledger/Common/Customers/All customers -
{company}/Purchase ledger/Vendors/All vendors -
{company}/Strategic asset management/Common/Contract planning/<Contract per specific status == Active>/Registration/Serial Number -
{company}/System administration/Area page/Periodic/Services and Application Integration Framework/History/{Select a <Port name>}/Document logs/{Select <Process step>}/{Select <View XML>} -
{company}/System administration/Area page/Periodic/Services and Application Integration Framework/Exceptions/{Select a <Exception message>} -
{company}/System administration/Area page/Setup/Services and Application Integration Framework/{Select <Inbound ports> or <Outbound ports}/{Select a <Port name>} -
{company}/System administration/Area page/Setup/System/Azure integration -
{company}/Organisation administration/Area page/Setup/Document management/Output document parameters -
Batch Jobs:
{company}/System administration/Area page/Enquires/Batch Jobs/<Job description> -
Customer Transactions:
{company}/Sales ledger/Common/Customers/All customers/[Lookup & Select customer]/[Click Transactions on the ribbon] -
Add Customer Relation To User:
{company}/System administration/Common/Users/Users then click on Relations under the Set Up button group and add the customer relation to the user and make it active.
-
-
Use
escto close any form -
Use
F12to get Table and Field information -
View Document Type Library Details:
{company}/Organisation administration/Area page/Setup/Document management/Document types/Output document parameter/<Right-Click Category Id>/<Click View details>{company}/ECM/Setup/ECM Parameters/SharePoint site collections
-
Extracting Infolog Messages
- In Dynamics 365 Finance & Operations (D365FO),
InfologDatais used to capture, store, and restore infolog messages programmatically, whileSysInfologEnumeratorallows you to iterate through those messages (errors, warnings, info) line by line in X++ for processing, filtering, or reporting.
- In Dynamics 365 Finance & Operations (D365FO),
-
Understanding the Security Hierarchy in D365FO
- Roles – Assigned to users; represent business responsibilities (e.g., Accounts Payable Clerk).
- Duties – Group tasks together; represent responsibilities within a role (e.g., Maintain vendor payments).
- Privileges – Lowest configurable level; define access to specific entry points (menu items, service operations, tables, etc.).
- Entry Points – Actual objects in the system (forms, reports, service operations, tables).
-
Dynamics AX Case Service Status OR Service Process Status Reset/Modification
- {company}/Case management/Area page/Setup/Case Update/Case admin update/{On the Overview tab, search for a case by Case ID and pick the detected case}/{Select the service on the Service Details tab, then click "Update Service Status" to update the service. To edit the status of a service process, go to the Process subtab and pick the process, then click the "Edit Process" button; if the modified service process is "Not started," ensure that the preceding process is "In progress". Alternatively, the process action parameters can be modified by selecting the "Update from setup" button, which will utilize the most recent list version of the process action parameters.}
-
Changing the URI of an AIF portal that has been disabled: Navigate to the AifPort table on path:
AOT/Data Dictionary/Tables/AifPort/{Select the port name}/{Deselect the enabled field}, then change the URI of the inbound port using the drop-down on path:{company}/System administration/Area page/Setup/ServicesandApplication Integration Framework/{Select the <Inbound port>/{Select the port name}. Sometimes the deployment/activation of the ports hangs, so confirming the files are deployed on the path,C:\Program Files\Microsoft Dynamics AX\60\AifWebServicesTest, and restarting the AOS can be sufficient.
-
To run a batch job in D365:
- Navigate to System administration > Enquiries > Batch jobs.
- Select the job.
- Set the Scheduled start date/time to when you want the job to begin.
- Under Recurrence, set it to run once and specify the end date as the same day.
- Under Change status, if the job status is Ended, you may need to create a new execution by setting its status to Waiting so that it re-enters the queue for execution.
-
To modify the WCF/AIF-Port configuration for a port, navigate to:
- Azure Function app time-out duration
System Administration > Setup > Services and Application Integration Framework > Inbound Ports. Choose the port you wish to edit, ensuring it is deactivated first. Next, click on Configure, proceed to Bindings, and adjust the timeout values as needed.
-
Debugging in Microsoft Dynamics AX 2012
- In cases where you need to debug X++ code in Interpreted mode, you can turn off the parameter Execute business operations in CIL. You can find this option in the development workspace.
Tools > Options > Development > General > Execute business operations in CIL > Disable
-
To resolve the error, during Team Explorer - Get and Map:
The path C:\AOSService\PackagesLocalDirectory is already mapped in workspace [WorkSpaceName]\[E-Mail_Address]- Delete the contents of this folder:
C:\Users\%UserName%\AppData\Local\Microsoft\Team Foundation\8.0\Cache - OR
C:\Users\%UserName%\AppData\Local\Microsoft\VisualStudio\16.xxx - OR
17.xxx\ComponentModelCache
- Delete the contents of this folder:
-
To edit a checked-out object by another developer using AOT, open table "
DEVToolsLockTable" and add a "x" at the end of the checked-out object name, restart client, complete changes, then when checking in, make sure to remove the "x" and on Deployment AOS, remove changes you did not make, as per deployment code version control standards. -
X++ and C# syntax differences
- X++:
System.Net.WebRequest httpRequest = System.Net.WebRequest::Create(fileUri); - C#:
System.Net.WebRequest httpRequest = System.Net.WebRequest.Create(fileUri);
- X++:
-
Delete the sub-folders found under your TFS profile here:
%localappdata%\Microsoft\Team Foundation\3.0\Cache -
All AX Tables:
{company}/System administration/Area page/<Data export/import>/<Definitions groups> -
AX Lookup MultiSelect Dialog:
AOT/Classes/tutorial_LookupMultiSelectDialog -
Switch off the CIL:
<Any Workspace>/Tools/Options/Development/<Execute business operations in CIL:>/<uncheck the checkbox>
- Clear AX usage data:
<Any Workspace>/Tools/Options/Development/<click Usage data on the action plane>/<click All usage data on the pop-up-form>/<select all records and delete>
- Add .NET Class Library Path:
C:\Program Files\Microsoft Dynamics AX\60\Server\MicrosoftDynamicsAX\bin
C:\Program Files (x86)\Microsoft Dynamics AX\60\Client\Bin
-
Another configuration is needed, the Table Name parameter needs to be filled in, so we don't have to create the record. This is the default way for the Portal, on
{company}/Case management/Area page/Setup/Services/Service table/[Seleceted service]/Service details/Table name/[Populate table name]. The prerequisites are that the field CaseServiceRecId must exist on the table as intended, and then the initValue is executed to pre-populate any values it can. -
DevOps find SQL file-filters:
file:{Name} ext:sql path:$/RepoName/Trunk/Development/Metadata/Model/Model/AxResource/ResourceContent/Data/
- AX 2012 Error: Could not load type 'Dynamics.Ax.Application.ClassName' from assembly 'Dynamics.Ax.Application, Version=0.0.0000.000, Culture=neutral, PublicKeyToken=null'.:
- To resolve the error, attempt to compile the ClassName and execute an Incremental CIL.
Faulted: System.OperationCanceledException: AIF service group not activated. Service group: <ServiceGroupName>. Error: Could not load type 'Dynamics.Ax.Application.<ClassName>' from assembly 'Dynamics.Ax.Application, Version=0.0.0000.000, Culture=neutral, PublicKeyToken=null'. ---> System.TypeLoadException: Could not load type 'Dynamics.Ax.Application.<ClassName>' from assembly 'Dynamics.Ax.Application, Version=0.0.0000.000, Culture=neutral, PublicKeyToken=null'.
-
D365 F&O SharingView Technical Brief
-
- The SharingView Concept
A
SharingView(e.g.,VENDTABLE_SharingView) is a kernel-generated SQL view. It enables Cross-Company Data Sharing for tables where data is physically stored per company but logically shared across a group.- Creation: Generated automatically when a Data Sharing Policy is activated (System Admin > Setup > Configure Data Sharing).
- Target: Created for tables where the property Save Data Per Company = Yes.
-
- Data Sharing Types
When configuring the policy in the UI, you choose how the
SharingViewhandles the underlying records:Sharing Type Technical Behavior Duplicate The kernel copies the record (and updates/deletes) across all companies in the sharing group. Each DataAreaIdhas its own physical row.Filtered (Rarely used) Only specific records meeting a criteria are shared. -
- Save Data Per Company (Table Property)
This property in Visual Studio determines the table's fundamental architecture:
- Yes (Default): The table includes a
DataAreaIdcolumn. Records are private to a Legal Entity unless aSharingViewis created to bridge them. - No: The table is Global. No
DataAreaIdexists (e.g.,DirPartyTable). Data is visible to all companies natively; no SharingView is needed or created.
-
- SQL Usage
Standard tables only show records for the specific
DataAreaIdin your session context. To see the "Consolidated" view across a sharing group via SQL:-- Query the SharingView to see synchronized records across the sharing group SELECT * FROM VENDTABLE_SharingView WHERE PARTITION = 5637144576 -- Mandatory for D365 multi-tenant architecture
Note: Do not attempt to
INSERTorUPDATEthe SharingView directly via SQL; always use the D365 X++ API or the Base Table to maintain data integrity.
-
Database Documentation
-- When investigating SQL Functions/Procedures, in cases where there may be missing data or data that has not been retrieved, consider commenting out the table join filters or the where clauses.
-- You can also try changing INNER JOIN to LEFT OUTER JOIN to catch rows that are missing related data:-- Index Hint
SELECT DPT.RecId ,DPT.PartyNumber ,CP.RecId ,CP.ContactPersonId ,CP.ContactForPARTY ,LEA.RecId ,LEA.[Locator] ,*
FROM DirPartyTable AS DPT WITH(INDEX(I_2303RECID,I_2303PARTYNUMBERIDX))
LEFT OUTER JOIN LogisticsElectronicAddress AS LEA ON LEA.RecId = DPT.PrimaryContactEmail
LEFT OUTER JOIN ContactPerson AS CP ON CP.Party = DPT.RecId
WHERE LEA.[Locator] = N'Email' AND DPT.PartyNumber = N'0';-- Paginate Results in T-SQL
SELECT * FROM CASEDETAILBASE ORDER BY RECID OFFSET 1000 ROWS FETCH NEXT 1000 ROWS ONLY;-- Find a Table Name by TABLEID (REFTABLEID) in Dynamics AX and return empty JSON (if not exists)
DECLARE @CURRENTTABLEID NVARCHAR(MAX) = N'<TABLEID>';
IF EXISTS (
SELECT 1
FROM dbo.SQLDICTIONARY
WHERE TABLEID = @CURRENTTABLEID
AND FIELDID = 0
)
BEGIN
SELECT (
SELECT [NAME]
FROM dbo.SQLDICTIONARY
WHERE TABLEID = @CURRENTTABLEID
AND FIELDID = 0
FOR JSON PATH
) AS RESULT;
END
ELSE
BEGIN
SELECT (
SELECT N'' AS [NAME]
FOR JSON PATH
) AS RESULT;
END-- Iterate through a list and split it using a comma as the separator
DECLARE @CURRENTPOSITION NVARCHAR(MAX);
DECLARE @LISTTABLE TABLE (
COLUMN1 NVARCHAR(MAX),
COLUMN2 NVARCHAR(MAX),
COLUMN3 NVARCHAR(MAX)
);
DECLARE LISTCURSOR CURSOR FOR
WITH [LIST] AS
(
SELECT VALUE
FROM STRING_SPLIT(N'INDEX1,INDEX2,INDEX3,INDEX4,INDEX5', N',')
)
SELECT [VALUE]
FROM [LIST];
OPEN LISTCURSOR;
FETCH NEXT FROM LISTCURSOR INTO @CURRENTPOSITION;
WHILE @@FETCH_STATUS = 0
BEGIN
BEGIN TRY
INSERT INTO @LISTTABLE VALUES (@CURRENTPOSITION, N'COLUMN2', N'COLUMN3');
-- INSERT INTO @LISTTABLE SELECT COLUMN1, COLUMN2, COLUMN3 FROM TABLE WHERE COLUMN1 = @CURRENTPOSITION AND COLUMN2 = N'1000';
FETCH NEXT FROM LISTCURSOR INTO @CURRENTPOSITION;
END TRY
BEGIN CATCH
PRINT CONCAT(N'ERROR NUMBER: ', ERROR_NUMBER(), N' ', N'CURRENT POSITION: ', @CURRENTPOSITION);
END CATCH
END;
CLOSE LISTCURSOR;
DEALLOCATE LISTCURSOR;
SELECT * FROM @LISTTABLE;-- Example 1: Scalar Function (Read-Only)
-- This returns the full name of a worker from `HcmWorker` based on `PersonnelNumber`:
-- SELECT dbo.GetWorkerFullName('12345') AS FullName;
CREATE FUNCTION dbo.GetWorkerFullName
(
@PersonnelNumber NVARCHAR(20)
)
RETURNS NVARCHAR(100)
AS
BEGIN
DECLARE @FullName NVARCHAR(100)
SELECT @FullName = FirstName + ' ' + LastName
FROM DirPersonName as PN
JOIN HcmWorker HW ON PN.PersonParty = HW.PersonParty
WHERE HW.PersonnelNumber = @PersonnelNumber
RETURN @FullName
END
GO-- Example 2: Table-Valued Function (Read-Only)
-- Return a table of all active workers in a specific department:
-- SELECT Workers.PersonnelNumber, Workers.LastName FROM dbo.GetActiveWorkersByDepartment('Finance') AS Workers;
CREATE FUNCTION dbo.GetActiveWorkersByDepartment
(
@DepartmentName NVARCHAR(100)
)
RETURNS TABLE
AS
RETURN
(
SELECT
HW.PersonnelNumber,
PN.FirstName,
PN.LastName,
OM.Name AS Department
FROM HcmWorker HW
JOIN DirPersonName PN ON HW.PersonParty = PN.PersonParty
JOIN OMOperatingUnit OM ON HW.PrimaryDepartment = OM.RecId
WHERE OM.Name = @DepartmentName
)
GO-- Get the Table ID for a given Table
SELECT TOP 1 TABLEID, NAME
FROM SQLDICTIONARY
WHERE NAME = N'DIRPARTYTABLE'
AND FIELDID = 0; -- Ensures you're getting the record for the table itself, not its fields.-- Get Class ID By Name and ID
SELECT TOP 100 * FROM MODELELEMENT
WHERE ELEMENTTYPE = 45 /*45 CORRESPONDS TO CLASSES*/
AND NAME = 'CaseDetailForm' AND ELEMENTHANDLE = 157680;-- System Enum Lookup: Definitions and Values
SELECT *
FROM [DBO].[ENUMIDTABLE] E
INNER JOIN [DBO].[ENUMVALUETABLE] EV
ON E.ID = EV.ENUMID
-- WHERE E.[NAME] = N'CaseCategoryType'
ORDER BY E.[NAME], EV.ENUMVALUE ASC;-- IF / ELSEIF / ELSE Logic in a `WHERE` Clause
-- Assume @FilterType can be 'Active', 'Inactive', or 'All'
-- SQL doesn't have native `IF/ELSE` inside a `WHERE` clause, but you can simulate it using `CASE` or conditional logic with `OR`/`AND`. Here's a simple example using a parameter like `@FilterType`:
SELECT AccountNum, Name, CustGroup
FROM CustTable
WHERE
(
(@FilterType = 'Active' AND CustTable.Blocked = 0) OR
(@FilterType = 'Inactive' AND CustTable.Blocked = 1) OR
(@FilterType = 'All')
);-- SWITCH CASE Using an `ENUM` in the `WHERE` Clause
-- CustVendorBlocked Enum values: 0 = NotBlocked, 1 = InvoiceBlocked, 2 = AllBlocked
SELECT AccountNum, Name, CustGroup
FROM CustTable
WHERE
CASE @BlockedStatus
WHEN 0 THEN CustTable.CustVendorBlocked = 0
WHEN 1 THEN CustTable.CustVendorBlocked = 1
WHEN 2 THEN CustTable.CustVendorBlocked = 2
ELSE 1 = 1 -- default: return all
END;
-- SQL Server doesn't allow `CASE` to directly return a boolean condition in `WHERE`, so you'd typically rewrite this using `OR` logic or dynamic SQL. Here's a more practical version:
-- WHERE
-- (
-- (@BlockedStatus = 0 AND CustVendorBlocked = 0) OR
-- (@BlockedStatus = 1 AND CustVendorBlocked = 1) OR
-- (@BlockedStatus = 2 AND CustVendorBlocked = 2)
-- );-- Querying Zip Code Records for City
SELECT TOP (100) *
FROM [dbo].[LOGISTICSADDRESSZIPCODE]
WHERE [CITY] = N'Rooihuiskraal'
--WHERE [ZIPCODE] = N'0154' OR [ZIPCODE] = N'154'
--WHERE [ZIPCODE] = N'0157' OR [ZIPCODE] = N'157'
ORDER BY [CITY] DESC;Source Code Documentation
Caesar Encryption Tutorial
Tutorial X++
private static str str2Caesar(str _source, int64 _shift = 1)
{
int maxChar = 65535;
int minChar = 0;
System.String _str = _source;
System.Char[] buffer = _str.ToCharArray();
for (int i = 0; i < buffer.Length; i++)
{
int shifted = System.Convert::ToInt32(buffer.GetValue(i)) + _shift;
if (shifted > maxChar)
{
shifted -= maxChar;
}
else if (shifted < minChar)
{
shifted += maxChar;
}
buffer.SetValue(System.Convert::ToChar(shifted), i);
}
str ret = new System.String(buffer);
return ret;
} Tutorial C#
private static string StringToCaesar(string source, Int16 shift = -1)
{
var maxChar = Convert.ToInt32(char.MaxValue);
var minChar = Convert.ToInt32(char.MinValue);
var buffer = source.ToCharArray();
for (var i = 0; i < buffer.Length; i++)
{
var shifted = Convert.ToInt32(buffer[i]) + shift;
if (shifted > maxChar)
{
shifted -= maxChar;
}
else if (shifted < minChar)
{
shifted += maxChar;
}
buffer[i] = Convert.ToChar(shifted);
}
return new string(buffer);
}D365FO – AX – X++ – Get File Extension List
public List getFileExtensionList()
{
#File
#ModelFile
ListEnumerator enumContainers;
container current;
int i;
str ext;
List extensionList = new List(Types::String);
container part1 = [#txt, #htm, #html, #rtf, #pdf, #dat, #def, #xpr, #xpo, #cfg, #log, #xml,
#config, #xsl, #asg, #chm, #ahd, #ald, #hlp, #dll, #exe, #msi, #sql, #awc];
container part2 = [#wsdl, #xls, #prf, #tiff, #mhtml, #wordDocumentExt, #wordTemplateExt,
#excelDocumentExt, #excelTemplateExt, #xlsx, #csv, #etl, #ias, #rdf, #eml,
#xslt, #tmp, #zip, #docx, #dotx, #odt, #ppt, #pptx, #pptm, #pot, #potx];
container part3 = [#odp, #xlsm, #ods, #odg, #jpg, #jpeg, #png, #gif, #bmp, #svg, #webp, #ico,
#psd, #ai, #eps, #tga, #mp3, #wav, #aac, #flac, #ogg, #m4a, #wma, #mid, #midi];
container part4 = [#mp4, #avi, #mov, #wmv, #flv, #mkv, #webm, #mpg, #mpeg, #_3gp, #mts,
#7z, #rar, #tar, #gz, #bz2, #xz, #iso, #dmg, #vmdk, #vdi, #vhd];
container part5 = [#js, #ts, #jsx, #tsx, #css, #scss, #less, #json, #jsonc, #yaml, #yml,
#md, #markdown, #php, #py, #rb, #java, #cs, #cpp, #c, #h, #go, #swift,
#sh, #bat, #ps1];
container part6 = [#db, #sqlite, #sqlite3, #mdb, #accdb, #dbf, #ttf, #otf, #woff, #woff2, #eot,
#ini, #conf, #cnf, #properties, #lock, #cache, #pid, #crt, #pem, #cer, #key,
#pfx, #msg, #pst, #ost, #vcf, #ics, #rss, #atom, #apk, #deb, #rpm];
List containers = new List(Types::Container);
containers.addEnd(part1);
containers.addEnd(part2);
containers.addEnd(part3);
containers.addEnd(part4);
containers.addEnd(part5);
containers.addEnd(part6);
enumContainers = containers.getEnumerator();
while (enumContainers.moveNext())
{
current = enumContainers.current();
for (i = 1; i <= conLen(current); i++)
{
ext = conPeek(current, i);
if (strStartsWith(ext, '.'))
{
ext = subStr(ext, 2, strLen(ext) - 1);
}
extensionList.addEnd(ext);
}
}
return extensionList;
}private boolean isValidExtension(str targetExt)
{
List extensionList = this.getFileExtensionList();
ListEnumerator enumerator = extensionList.getEnumerator();
// Linear Search
while (enumerator.moveNext())
{
if (strLwr(enumerator.current()) == strLwr(targetExt))
{
return true;
}
}
return false;
}D365FO – AX – X++ – Get All AOT Resources
https://pedrotornich.com/a/2019/09/26/use-d365fo-metadata-api-in-net/ https://stackoverflow.com/questions/45598166/how-to-get-aot-objects-tables-for-example-belongs-to-a-model-in-ax7 || https://stackoverflow.com/a/45712653
// C#
// Required references:
// C:\AOSService\webroot\bin\Microsoft.Dynamics.ApplicationPlatform.Environment.dll
// C:\AOSService\webroot\bin\Microsoft.Dynamics.AX.Metadata.dll
// C:\AOSService\webroot\bin\Microsoft.Dynamics.AX.Metadata.Core.dll
// C:\AOSService\webroot\bin\Microsoft.Dynamics.AX.Metadata.Storage.dll
using Microsoft.Dynamics.AX.Metadata.Storage;
using Microsoft.Dynamics.AX.Metadata.Storage.Runtime;
using System;
using System.Diagnostics;
namespace ConsoleApp
{
public class Program
{
public static void Main(string[] args)
{
try
{
var environment = Microsoft.Dynamics.ApplicationPlatform.Environment.EnvironmentFactory.GetApplicationEnvironment();
var runtimeConfiguration = new RuntimeProviderConfiguration(environment.Aos.PackageDirectory);
var metadataProvider = new MetadataProviderFactory().CreateRuntimeProviderWithExtensions(runtimeConfiguration);
var aotResources = metadataProvider.Resources.ListObjectsForModel("ModelPrefix");
foreach (var resource in aotResources)
{
Debug.WriteLine(string.Concat("Resource Name: ", resource));
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
}
}// X++ - AX2012
static void getAllAOTResources(Args _args)
{
#AOT
TreeNode treeNode;
TreeNode resourcesNode;
TreeNodeIterator iterator;
// resourcesNode = TreeNode::findNode(@"\Resources");
resourcesNode = TreeNode::findNode(#ResourcesPath);
iterator = resourcesNode.AOTiterator();
while (iterator.next())
{
treeNode = iterator.next();
info(treeNode.AOTname());
}
}// X++ - D365
public final class GetAllAOTResources
{
public static void main(Args _args)
{
var environment = Microsoft.Dynamics.ApplicationPlatform.Environment.EnvironmentFactory::GetApplicationEnvironment();
var runtimeConfiguration = new Microsoft.Dynamics.AX.Metadata.Storage.Runtime.RuntimeProviderConfiguration(environment.get_Aos().get_PackageDirectory());
var metadataProviderFactory = new Microsoft.Dynamics.AX.Metadata.Storage.MetadataProviderFactory();
var metadataProvider = metadataProviderFactory.CreateRuntimeProvider(runtimeConfiguration);
var aotResources = metadataProvider.Resources.ListObjectsForModel("ModelPrefix");
System.Collections.IEnumerator enumerator = aotResources.GetEnumerator();
while (enumerator.MoveNext())
{
str resourceName = enumerator.Current;
if (strStartsWith(resourceName, "potentialStart") || strStartsWith(resourceName, "potentialStart") || strStartsWith(resourceName, "potentialStart"))
{
ResourceNode resourceNode = SysResource::getResourceNode(resourceName);
if(resourceNode && strEndsWith(resourceNode.filename(), "potentialEnd"))
{
BinData binData = new BinData();
container resourceNodeData = SysResource::getResourceNodeData(resourceNode);
binData.setData(resourceNodeData);
str result = binData.getStrData();
info(result);
}
}
}
}
}D365FO – AX – X++ – JSON Construct Tutorial
static void jsonConstructTutorial(Args _args)
{
System.IO.StringWriter strWriter;
Newtonsoft.Json.JsonTextWriter jsonWriter;
System.Exception exception;
str jsonBody;
Map jsonBodyMap;
MapEnumerator jsonBodyEnumer;
str jsonBodyMapName;
str jsonBodyMapValue;
// JSON Array Deconstruct Tutorial - Begin
int i, arrayLength;
str txtName, txtModel;
Newtonsoft.Json.Linq.JObject jObject;
Newtonsoft.Json.Linq.JToken sName, sModel;
// JSON Array Deconstruct Tutorial - End
// JSON Array Deconstruct Tutorial - Begin
str jsString = '[{"Name": "BMW", "Model": "325IS"}, {"Name": "VW", "Model": "City Golf"}]';
Newtonsoft.Json.Linq.JArray jArry = Newtonsoft.Json.Linq.JArray::Parse(jsString);
// JSON Array Deconstruct Tutorial - End
// JSON Deconstruct Tutorial - Begin
str jsonString = '{"Name": "Jack Moose", "Age": 30, "Cars": [{"Name": "BMW", "Model": "325IS"}, {"Name": "VW", "Model": "City Golf"}]}';
Newtonsoft.Json.Linq.JObject jObjct = Newtonsoft.Json.Linq.JObject::Parse(jsonString);
Newtonsoft.Json.Linq.JToken jToken = jObjct.get_Item('Cars');
Newtonsoft.Json.Linq.JArray jArray = jToken as Newtonsoft.Json.Linq.JArray;
Newtonsoft.Json.Linq.JObject subObjct = jArray.get_First();
Newtonsoft.Json.Linq.JToken subName = subObjct.get_Item('Name');
Newtonsoft.Json.Linq.JToken subModel = subObjct.get_Item('Model');
info(subModel.ToString());
// JSON Deconstruct Tutorial - End
// JSON Array Deconstruct Tutorial - Begin
// Loop through each item in the array and log the "Name" and "Model" elements
arrayLength = jArry.get_Count();
for (i = 0; i < arrayLength; i++)
{
jObject = jArray.get_Item(i);
sName = jObject.get_Item('Name');
sModel = jObject.get_Item('Model');
txtName = sName.ToString();
txtModel = sModel.ToString();
info(strFmt("Name: %1 Model: %2", txtName, txtModel));
}
// JSON Array Deconstruct Tutorial - End
// JSON Construct Tutorial
try
{
strWriter = new System.IO.StringWriter();
jsonWriter = new Newtonsoft.Json.JsonTextWriter(strWriter);
jsonBody = strMax();
jsonBodyMap = new Map(Types::String, Types::String);
jsonBodyMap.insert("ScopeEmail", "email");
jsonBodyMap.insert("ScopeOpenId", "openid");
jsonBodyMap.insert("ScopeOfflineAccess", "offline_access");
jsonBodyMap.insert("ScopeImap", "https://outlook.office.com/IMAP.AccessAsUser.All");
jsonBodyMap.insert("ScopeSmtp", "https://outlook.office.com/SMTP.Send");
jsonBodyMap.insert("SmtpHost", "smtp.office365.com");
jsonBodyMap.insert("SmtpPort", "");
jsonBodyMap.insert("ImapHost", "outlook.office365.com");
jsonBodyMap.insert("ImapPort", "993");
jsonBodyMap.insert("TenantId", "");
jsonBodyMap.insert("AppId", "");
jsonBodyMap.insert("AppSecret", "");
jsonBodyMap.insert("Username", "");
jsonBodyMap.insert("Password", "");
jsonWriter.WriteStartObject();
jsonBodyEnumer = jsonBodyMap.getEnumerator();
while (jsonBodyEnumer.moveNext())
{
jsonBodyMapName = jsonBodyEnumer.currentKey();
jsonBodyMapValue = jsonBodyEnumer.currentValue();
jsonWriter.WritePropertyName(jsonBodyMapName);
jsonWriter.WriteValue(jsonBodyMapValue);
}
jsonWriter.WritePropertyName("Email");
jsonWriter.WriteStartObject();
jsonWriter.WritePropertyName("To");
jsonWriter.WriteValue("tyre@contoso.co.za");
jsonWriter.WritePropertyName("Subject");
jsonWriter.WriteValue("Test Email");
jsonWriter.WritePropertyName("Body");
jsonWriter.WriteValue("Hello world!");
jsonWriter.WriteEndObject();
jsonWriter.WriteEndObject();
jsonBody = strWriter.ToString();
info(jsonBody);
}
catch
{
error("@EXD32721");
exception = ClrInterop::getLastException();
if (exception != null)
{
exception = exception.get_InnerException();
if (exception != null)
{
error("\n" + clr2XppStr(exception.ToString()));
}
}
}
}D365FO – AX – X++ – Tutorial: reread, research
[ExtensionOf(formcontrolstr(LogisticsPostalAddress, RefreshForm))]
final class LogisticsPostalAddress_RefreshForm_FC_Extension
{
void clicked()
{
// https://d365ffo.com/2021/04/02/d365ffo-ax-x-tutorial-refresh-reread-research-executequery-which-one-to-use/comment-page-1/
FormRun formRun;
FormControl formControl;
// FormDataObject formDataObject;
FormDataSource formDataSource;
next clicked();
// formControl = this.formRun().design().controlName("FormControlName") as FormControl;
// formControl = this.formRun().design().controlName(formControlStr(FormName, FormControlName)) as FormControl;
formControl = any2Object(this) as FormControl;
// formDataObject = any2Object(this) as FormDataObject;
formRun = formControl.formRun().args().caller();
// formRun = formDataObject.datasource().formRun();
formDataSource = formRun.dataSource();
formDataSource.reread();
formDataSource.research();
// formRun.doRefresh();
// element.redraw();
// element.reload();
}
}D365FO – AX – X++ – SysOperation Framework
- When To Use SysOperation Framework in D365
First, what are some scenarios where you might want a process or job to run in the background?
- The most common scenario is that you have a set of records that you need to process or do some calculation for. Perhaps you have used a Data Entity or Service to bring data into a staging table. Now you need to loop through each record and create functional data inside the system. SysOperation Framework in D365 is made for just this sort of thing.
- Another scenario is that you have some process that you normally run on a single record in a form, but now you want to run the process on many records. SysOperation Framework in D365 can easily be setup to loop through a group of records and call existing code to process each record.
- Lastly, is a scenario that is often under used. You may have a button on a form that when clicked will run some process on a record. If that process does not run instantly and you do not need to immediately use the result of the process, you can offload that work to job that uses SysOperation Framework in D365. Perhaps running a single process only takes 15 seconds to run, but by letting the system do the work in the background you can improve the user experience greatly. Typically a user would have to wait those 15 seconds while the operation completes. But now they are given back control immediately and can continue working.
The contract class contains any data we need to directly send to the process. Create a new class in D365, with the attribute [DataContractAttribute] at the top. And by convention the name of the class should end with the word ‘Contract’. Add member variables and parm methods for each value that needs to be passed to the process doing the work. Perhaps you have a From Date and To Date that you are running the process for. In the simplest SysOperation Framework scenarios, this class can be left blank.
[DataContractAttribute]
class rsmTutSysOperationsContract
{
private int retentionDays;
[DataMember,
SysOperationLabel(literalStr("Retention days")),
SysOperationHelpText(literalStr("Data older than the retention days will be cleaned up.")),
SysOperationDisplayOrder('1')]
public int parmRetentionDays(int _retentionDays = retentionDays)
{
retentionDays = _retentionDays;
return retentionDays;
}
}The service class contains the code that actually performs the processing. Create a new class in D365, that extends the class SysOperationServiceBase. Also, by convention, the name of the class should end with the word ‘Service’. Create a public method, named whatever you would like, that takes a single parameter which is of the type of the Contract class.
class rsmTutSysOperationsService extends SysOperationServiceBase
{
public void process(rsmTutSysOperationsContract _contract)
{
//Do something
int retentionDays = _contract.parmRetentionDays();
Info(strFmt("Retention days: %1", retentionDays));
}
}The controller class really just configures the SysOperation Framework process. Create a new class in D365, that extends the class SysOperationServiceController.
class rsmTutSysOperationsController extends SysOperationServiceController
{
}Then add these four methods.
- Create a new method that calls super. It needs to pass a)The name of the Service class, the name of the method on the service Class, and the execution mode.
protected void new()
{
super(classStr(rsmTutSysOperationsService), methodStr(rsmTutSysOperationsService, process), SysOperationExecutionMode::Synchronous);
}- Create a method named ‘defaultCaption’. This will override the base method and return the name of the job.
public ClassDescription defaultCaption()
{
return "Process Job";
}- Create a method named ‘construct’. This will instantiate the controller class and set the execution mode.
public static rsmTutSysOperationsController construct(SysOperationExecutionMode _executionMode = SysOperationExecutionMode::Synchronous)
{
rsmTutSysOperationsController controller;
controller = new rsmTutSysOperationsController();
controller.parmExecutionMode(_executionMode);
return controller;
}- Lastly, create a method named ‘main’. The main method is always called by the menu item. This method will call ‘startOperation’ which actually calls the Service class’s method to do the work.
public static void main(Args _args)
{
rsmTutSysOperationsController controller;
controller = rsmTutSysOperationsController::construct();
controller.parmArgs(_args);
controller.startOperation();
}Create a new action menu item that will call the controller class. In my example, the controller class is named rsmTutSysOperationsController. So I will name my action menu item the same thing. In the Properties, set the ‘Object Type’ to be Class, and the ‘Object’ to be name of the controller class.
Set the Label in the menu item properties. Then add the menu item to an appropriate menu.
Please see these other articles for more advanced SysOperation Framework functionality.
First, learn how to add parameters to your batch jobs. D365 SysOperation Framework Parameters
Next, override the drop-downs to show only validate options. D365 SysOperation Framework Override Lookups
After adding parameters to a SysOperation Framework batch job, you may want to set those parameters to start with an initial value. Learn how to set a D365 SysOperation Framework default value.
Additionally, add validation to the parameters. D365 SysOperation Framework Validation
Lastly, learn to add D365 SysOperation Framework mandatory parameters that show a red outline and a star to indicate the field must be populated.
For another great reference on how to create classes using the SysOperation Framework in D365, see this article.
D365FO – AX – X++ – Caching display methods
// Option 1 - For this, a corresponding call should always be integrated into the init() method of a form datasource
public void init()
{
super();
this.cacheAddMethod(tableMethodStr(DirPartyPostalAddressView,locationRoles));
}// Option 2 - Set a corresponding attribute in the table display method itself
[SysClientCacheDataMethodAttribute(true)]
public client server display ExternalItemId defaultExternalItemId()
{
return this.defaultCustVendExternalItemDescription().externalItemId();
}D365FO – AX – X++ – Azure Blob Storage REST API
-
A generic
curlexample for listing blobs in an Azure Storage container using the REST API with Shared Key authenticationcurl -X GET "https://<storage-account>.blob.core.windows.net/<container-name>?restype=container&comp=list&prefix=<folder-path>/" \ -H "x-ms-date: <RFC-1123-date>" \ -H "x-ms-version: 2021-10-04" \ -H "Authorization: SharedKey <storage-account>:<signature>"
- Replace the placeholders:
<storage-account>: Your Azure Storage account name<container-name>: The name of your blob container<folder-path>: The virtual folder path inside the container<RFC-1123-date>: Current date in RFC 1123 format (e.g.,Thu, 02 Oct 2025 09:28:00 GMT)<signature>: HMAC-SHA256 signature generated from the string-to-sign using your storage account key
- Replace the placeholders:
-
To authenticate Azure Storage REST API calls using App Registration instead of Shared Key, you’ll use OAuth 2.0 with Microsoft Entra ID (formerly Azure AD). Here's a step-by-step guide to make it work:
-
Step 1: Create an App Registration
- Go to Azure Portal.
- Navigate to Microsoft Entra ID → App registrations → New registration.
- Give it a name and register it.
- Note the Application (client) ID and Directory (tenant) ID.
-
Step 2: Create a Client Secret
- In the App Registration → Certificates & secrets.
- Click New client secret, give it a name, and set an expiry.
- Copy the secret value immediately — you won’t see it again.
-
Step 3: Assign RBAC Role to the App
- Go to your Storage Account → Access Control (IAM).
- Click Add role assignment.
- Choose a role like:
Storage Blob Data Reader(for read-only)Storage Blob Data Contributor(for read/write)
- Assign it to your App Registration (search by name).
-
Step 4: Acquire OAuth Token Use
curlor Postman to get a token:curl -X POST https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "client_id=<client-id>&scope=https://storage.azure.com/.default&client_secret=<client-secret>&grant_type=client_credentials"
This returns an
access_token. -
Step 5: Make Authenticated
List BlobsREST API Callcurl -X GET "https://<storage-account>.blob.core.windows.net/<container>?restype=container&comp=list&prefix=<folder-path>/" \ -H "Authorization: Bearer <access_token>" \ -H "x-ms-version: 2025-07-05"
-
Step 6: to Move a Blob Using REST API
-
Copy the Blob to the New Location
- Use the
Put Blob From URLoperation with thex-ms-copy-sourceheader. - This initiates a server-side copy of the blob to the new destination path (which simulates a folder).
PUT https://<storage-account>.blob.core.windows.net/<container>/<new-folder>/<blob-name> x-ms-copy-source: https://<storage-account>.blob.core.windows.net/<container>/<old-folder>/<blob-name> - Use the
-
Delete the Original Blob
- Once the copy operation completes successfully, use the
Delete Bloboperation to remove the original blob.
DELETE https://<storage-account>.blob.core.windows.net/<container>/<old-folder>/<blob-name> - Once the copy operation completes successfully, use the
-
-
Step 7: X++ Version
Source Code
class ListBlobContract { str name; utcDateTime creationTime; utcDateTime lastModified; str etag; int64 contentLength; str contentType; str contentEncoding; str contentLanguage; str contentCRC64; str contentMD5; str cacheControl; str contentDisposition; str blobType; str accessTier; boolean accessTierInferred; str leaseStatus; str leaseState; boolean serverEncrypted; public str parmName(str _name = name) { name = _name; return name; } public utcDateTime parmCreationTime(utcDateTime _creationTime = creationTime) { creationTime = _creationTime; return creationTime; } public utcDateTime parmLastModified(utcDateTime _lastModified = lastModified) { lastModified = _lastModified; return lastModified; } public str parmEtag(str _etag = etag) { etag = _etag; return etag; } public int64 parmContentLength(int64 _contentLength = contentLength) { contentLength = _contentLength; return contentLength; } public str parmContentType(str _contentType = contentType) { contentType = _contentType; return contentType; } public str parmContentEncoding(str _contentEncoding = contentEncoding) { contentEncoding = _contentEncoding; return contentEncoding; } public str parmContentLanguage(str _contentLanguage = contentLanguage) { contentLanguage = _contentLanguage; return contentLanguage; } public str parmContentCRC64(str _contentCRC64 = contentCRC64) { contentCRC64 = _contentCRC64; return contentCRC64; } public str parmContentMD5(str _contentMD5 = contentMD5) { contentMD5 = _contentMD5; return contentMD5; } public str parmCacheControl(str _cacheControl = cacheControl) { cacheControl = _cacheControl; return cacheControl; } public str parmContentDisposition(str _contentDisposition = contentDisposition) { contentDisposition = _contentDisposition; return contentDisposition; } public str parmBlobType(str _blobType = blobType) { blobType = _blobType; return blobType; } public str parmAccessTier(str _accessTier = accessTier) { accessTier = _accessTier; return accessTier; } public boolean parmAccessTierInferred(boolean _accessTierInferred = accessTierInferred) { accessTierInferred = _accessTierInferred; return accessTierInferred; } public str parmLeaseStatus(str _leaseStatus = leaseStatus) { leaseStatus = _leaseStatus; return leaseStatus; } public str parmLeaseState(str _leaseState = leaseState) { leaseState = _leaseState; return leaseState; } public boolean parmServerEncrypted(boolean _serverEncrypted = serverEncrypted) { serverEncrypted = _serverEncrypted; return serverEncrypted; } public static List parseListBlobsXml(str xmlResponse) { System.Xml.XmlNodeList blobNodes; System.Xml.XmlNode blobNode, propNode, prefixNode; System.Xml.XmlNode node; str nodeInnerText, prefixNodeInnerText; ListBlobContract listBlobContract; int i, blobCount; Filename fileName, fileType, filePath; System.Xml.XmlDocument xmlDoc = new System.Xml.XmlDocument(); List blobList = new List(Types::Class); xmlDoc.LoadXml(xmlResponse); prefixNode = xmlDoc.SelectSingleNode("//Prefix"); if (prefixNode != null) { prefixNodeInnerText = prefixNode.get_InnerText(); } blobNodes = xmlDoc.SelectNodes("//Blob"); blobCount = blobNodes.get_Count(); for (i = 0; i < blobCount; i++) { blobNode = blobNodes.Item(i); propNode = blobNode.SelectSingleNode("Properties"); listBlobContract = new ListBlobContract(); node = blobNode.SelectSingleNode("Name"); if (node != null) { listBlobContract.parmName(node.get_InnerText()); } [fileName, fileType, filePath] = Docu::splitFilename(listBlobContract.parmName()); prefixNodeInnerText = (substr(prefixNodeInnerText, strlen(prefixNodeInnerText), 1) == '/') ? substr(prefixNodeInnerText, 1, strlen(prefixNodeInnerText) - 1) : prefixNodeInnerText; // Remove a trailing slash filePath = (substr(filePath, strlen(filePath), 1) == '/') ? substr(filePath, 1, strlen(filePath) - 1) : filePath; // Remove a trailing slash if (strLen(listBlobContract.parmName()) > 0 && strCmp(strLwr(prefixNodeInnerText), strLwr(filePath)) == 0 ) // To avoid listing blobs from subfolders { node = propNode.SelectSingleNode("Creation-Time"); nodeInnerText = node.get_InnerText(); if (node != null) { listBlobContract.parmCreationTime(System.DateTime::Parse(nodeInnerText)); } node = propNode.SelectSingleNode("Last-Modified"); nodeInnerText = node.get_InnerText(); if (node != null) { listBlobContract.parmLastModified(System.DateTime::Parse(nodeInnerText)); } node = propNode.SelectSingleNode("Etag"); if (node != null) { listBlobContract.parmEtag(node.get_InnerText()); } node = propNode.SelectSingleNode("Content-Length"); nodeInnerText = node.get_InnerText(); if (node != null) { listBlobContract.parmContentLength(str2int64(nodeInnerText)); } node = propNode.SelectSingleNode("Content-Type"); if (node != null) { listBlobContract.parmContentType(node.get_InnerText()); } node = propNode.SelectSingleNode("Content-Encoding"); if (node != null) { listBlobContract.parmContentEncoding(node.get_InnerText()); } node = propNode.SelectSingleNode("Content-Language"); if (node != null) { listBlobContract.parmContentLanguage(node.get_InnerText()); } node = propNode.SelectSingleNode("Content-CRC64"); if (node != null) { listBlobContract.parmContentCRC64(node.get_InnerText()); } node = propNode.SelectSingleNode("Content-MD5"); if (node != null) { listBlobContract.parmContentMD5(node.get_InnerText()); } node = propNode.SelectSingleNode("Cache-Control"); if (node != null) { listBlobContract.parmCacheControl(node.get_InnerText()); } node = propNode.SelectSingleNode("Content-Disposition"); if (node != null) { listBlobContract.parmContentDisposition(node.get_InnerText()); } node = propNode.SelectSingleNode("BlobType"); if (node != null) { listBlobContract.parmBlobType(node.get_InnerText()); } node = propNode.SelectSingleNode("AccessTier"); if (node != null) { listBlobContract.parmAccessTier(node.get_InnerText()); } node = propNode.SelectSingleNode("AccessTierInferred"); nodeInnerText = node.get_InnerText(); if (node != null) { listBlobContract.parmAccessTierInferred(nodeInnerText == "true"); } node = propNode.SelectSingleNode("LeaseStatus"); if (node != null) { listBlobContract.parmLeaseStatus(node.get_InnerText()); } node = propNode.SelectSingleNode("LeaseState"); if (node != null) { listBlobContract.parmLeaseState(node.get_InnerText()); } node = propNode.SelectSingleNode("ServerEncrypted"); nodeInnerText = node.get_InnerText(); if (node != null) { listBlobContract.parmServerEncrypted(nodeInnerText == "true"); } blobList.addEnd(listBlobContract); } } return blobList; } }
class AzureBlobUtility { public static str getAccessToken() { System.Net.WebClient webClient; System.Collections.Specialized.NameValueCollection values; System.Text.UTF8Encoding encoding; System.Byte[] responseBytes; str responseText; Newtonsoft.Json.Linq.JObject json; System.Object accessTokenObj; CLRObject clrException; str tenantId = ""; str clientId = ""; str clientSecret = ""; str scope = "https://storage.azure.com/.default"; str tokenEndpoint; try { tokenEndpoint = strFmt( "https://login.microsoftonline.com/%1/oauth2/v2.0/token", tenantId ); webClient = new System.Net.WebClient(); values = new System.Collections.Specialized.NameValueCollection(); values.Add("client_id", clientId); values.Add("scope", scope); values.Add("client_secret", clientSecret); values.Add("grant_type", "client_credentials"); responseBytes = webClient.UploadValues(tokenEndpoint, "POST", values); encoding = new System.Text.UTF8Encoding(); responseText = encoding.GetString(responseBytes); json = Newtonsoft.Json.Linq.JObject::Parse(responseText); accessTokenObj = json.get_Item("access_token"); return accessTokenObj.ToString(); } catch (Exception::CLRError) { clrException = CLRInterop::getLastException(); error(clrException.ToString()); return ""; } } public static List listBlobs(str storageAccount, str containerName, str folderPath, str accessToken, int maxResults = 5000) { str url; List list; str responseStr, nextMarker; System.Exception exception; System.Xml.XmlNode nextMarkerNode; System.Net.WebHeaderCollection headers = new System.Net.WebHeaderCollection(); System.Net.WebClient webClient = new System.Net.WebClient(); System.Xml.XmlDocument xmlDoc = new System.Xml.XmlDocument(); try { url = strFmt("https://%1.blob.core.windows.net/%2?restype=container&comp=list&prefix=%3&maxresults=%4", storageAccount, containerName, folderPath, maxResults ); headers.Add("Authorization", strFmt("Bearer %1", accessToken)); headers.Add("x-ms-version", "2025-07-05"); webClient.set_Headers(headers); responseStr = webClient.DownloadString(url); xmlDoc.LoadXml(responseStr); nextMarkerNode = xmlDoc.SelectSingleNode("//NextMarker"); if (nextMarkerNode != null) { nextMarker = nextMarkerNode.get_InnerText(); } // A string value that identifies the portion of the list to be returned with the next list operation. // The operation returns a marker value within the response body if the list returned was not complete. list = ListBlobContract::parseListBlobsXml(responseStr); return list; } catch (Exception::CLRError) { exception = CLRInterop::getLastException(); error(exception.ToString()); return new List(Types::Class); } } public static boolean copyBlob(str storageAccount, str containerName, str oldFolder, str newFolder, str blobName, str accessToken) { str sourceUrl; str destUrl; System.Exception exception; System.Net.WebHeaderCollection headers = new System.Net.WebHeaderCollection(); System.Net.WebClient webClient = new System.Net.WebClient(); try { sourceUrl = strFmt("https://%1.blob.core.windows.net/%2/%3/%4", storageAccount, containerName, oldFolder, blobName); destUrl = strFmt("https://%1.blob.core.windows.net/%2/%3/%4", storageAccount, containerName, newFolder, blobName); headers.Add("Authorization", strFmt("Bearer %1", accessToken)); headers.Add("x-ms-version", "2025-07-05"); headers.Add("x-ms-copy-source", sourceUrl); webClient.set_Headers(headers); webClient.UploadString(destUrl, "PUT", ""); return true; } catch (Exception::CLRError) { exception = CLRInterop::getLastException(); error(exception.ToString()); return false; } } public static boolean deleteBlob(str storageAccount, str containerName, str folder, str blobName, str accessToken) { str url; System.Exception exception; System.Net.WebHeaderCollection headers = new System.Net.WebHeaderCollection(); System.Net.WebClient webClient = new System.Net.WebClient(); try { url = strFmt("https://%1.blob.core.windows.net/%2/%3/%4", storageAccount, containerName, folder, blobName); headers.Add("Authorization", strFmt("Bearer %1", accessToken)); headers.Add("x-ms-version", "2025-07-05"); webClient.set_Headers(headers); webClient.UploadString(url, "DELETE", ""); return true; } catch (Exception::CLRError) { exception = CLRInterop::getLastException(); error(exception.ToString()); return false; } } public static void main(Args _args) { CLRObject clrException; str accessToken; ListEnumerator listEnumerator; List list; ListBlobContract listBlobContract; Filename fileName, fileNameWithoutExt, fileType, filePath; try { accessToken = AzureBlobUtility::getAccessToken(); // Valid for 1 hour list = AzureBlobUtility::listBlobs("storage-account", "container-name", "root-folder/sub-folder1", accessToken); listEnumerator = list.getEnumerator(); while (listEnumerator.moveNext()) { listBlobContract = listEnumerator.current(); fileName = System.IO.Path::GetFileName(listBlobContract.parmName()); [fileNameWithoutExt, fileType, filePath] = Docu::splitFilename(listBlobContract.parmName()); if (strCmp(strLwr(fileName), strLwr("file.pdf")) == 0) { info(listBlobContract.parmName()); AzureBlobUtility::copyBlob("storage-account", "container-name", "root-folder/sub-folder1", "root-folder/sub-folder2", fileName, accessToken); AzureBlobUtility::deleteBlob("storage-account", "container-name", "root-folder/sub-folder1", fileName, accessToken); } } } catch (Exception::CLRError) { clrException = CLRInterop::getLastException(); error(clrException.ToString()); } } }
-
D365FO – AX – X++ – Custom Service (SOAP/JSON)
In this article I will show you how to create a custom service in D365.
Unlike calling a Data Entity which can create or retrieve data from tables, services allow us to run code within Microsoft Dynamics 365. This is very useful if for example you need to retrieve a value that is not stored in a table. Or you would like to run a process, and return whether it was successful or not.
In order to create a custom service in D365, there are several pieces we need.
- We need a new class that is our ‘Request‘ object. The data we send in will be mapped to the variables in this class. Then we will use this class to run our operation.
- We need a new calls that is our ‘Response‘ object. In our code we will set the variables in this class to contain whatever information we want returned to the calling program. D365 will then convert these values into JSON data.
- We need a Service class. This is the class where we will put the code that will: a) read the data in our Request object, b) run some process, and then c) populate the Response object.
- We need a Service object, that points to our Service class. This lets Dynamics 365 know that our class can be called by an outside system.
- Lastly, we need a Service Group to put our Service object into.
When you create a custom service in D365, the Request class needs to have a variable for each piece of data you are sending into the system.
First, create a new class in Visual Studio. I named my ‘rsmTutRequestSimple’.
Next, add the Attribute [DataContractAttribute] just above the class declaration.
Add a variable. I named mine ‘dataAreaId’.
Finally, create a parameter method for that variable, and put the attribute [DataMember(““)] above the method.
Your code should now look like this:
[DataContractAttribute]
class rsmTutRequestList
{
private List requests;
[DataMemberAttribute("Requests"),
AifCollectionTypeAttribute("_requests", Types::Class, classStr(rsmTutRequestSimple)),
AifCollectionTypeAttribute("return", Types::Class, classStr(rsmTutRequestSimple))]
public List parmRequests(List _value = requests)
{
if (!prmIsDefault(_value))
{
requests = _value;
}
return requests;
}
}[DataContractAttribute]
class rsmTutRequestSimple
{
private str dataAreaId;
[DataMember("DataAreaId")]
public str parmDataAreaId(str _value = dataAreaId)
{
if (!prmIsDefault(_value))
{
dataAreaId = _value;
}
return dataAreaId;
}
}The Response class needs to have a variable for each piece of information we would like to send back to the calling program.
First, create a new class in Visual Studio. I named mine ‘rsmTutReponse’.
Add the attribute [DataContractAttribute] just above the class definition.
Add variables that will contain information you want to return to the calling program. I created variables ‘success, ‘errorMessage’, and ‘debugMessage’.
Finally, create a parameter method for each of your variables. Put the attribute [DataMember(““)] above each method definition. The value you put as will be the name of the JSON tag that is generated.
Your code should now look like this:
[DataContractAttribute]
class rsmTutResponse
{
private boolean success;
private str errorMessage;
private str debugMessage;
[DataMember("ErrorMessage")]
public str parmErrorMessage(str _value = errorMessage)
{
if (!prmIsDefault(_value))
{
errorMessage = _value;
}
return errorMessage;
}
[DataMember("Success")]
public Boolean parmSuccess(Boolean _value = success)
{
if (!prmIsDefault(_value))
{
success = _value;
}
return success;
}
[DataMember("DebugMessage")]
public str parmDebugMessage(str _value = debugMessage)
{
if (!prmIsDefault(_value))
{
debugMessage = _value;
}
return debugMessage;
}
}The Serviceclass is the most interesting class when you create a custom service in D365. It is responsible for reading values from the Requestobject, running some process, then setting values on the Responseobject.
First, create a new class in Visual Studio. I named mine ‘rsmTutServiceSimple’.
Create a method in the class, named whatever you would like. I named mine ‘create’.
The method needs to take as a parameter which has a type of your Request object. And it needs to return a value which has a type of your Response object. So for example this:
public rsmTutResponse create(rsmTutRequestList _requestList) Next, you can put whatever code you need to inside the method. In my example, I am reading the dataAreaId from the request object, and then I am storing the text “Hello World” in the response object.
class rsmTutServiceSimple
{
public rsmTutResponse create(rsmTutRequestList _requestList)
{
System.Exception interopException;
RsmTutTable rsmTutTable;
ListEnumerator listEnumerator;
SysInfologEnumerator sysInfologEnumerator;
SysInfologMessageStruct sysInfologMessageStruct;
RsmTutRequestSimple request;
str retMsg;
RsmTutResponse response = new rsmTutResponse();
boolean ret = true;
listEnumerator = _requestList.parmRequests().getEnumerator();
while (listEnumerator.moveNext())
{
request = listEnumerator.current();
changecompany(request.parmDataAreaId())
{
try
{
response.parmSuccess(ret);
response.parmDebugMessage("Hello World");
rsmTutTable.clear();
rsmTutTable.initValue();
rsmTutTable.DebugMessage = response.parmDescription();
rsmTutTable.Success = response.parmFileFormat();
if (rsmTutTable.validateWrite())
{
ttsbegin;
rsmTutTable.insert();
ttscommit;
}
}
catch (Exception::DuplicateKeyException)
{
retMsg = strFmt("@SYS339391",
"",
"",
"",
"",
"");
ret = checkFailed(retMsg);
response.parmSuccess(ret);
response.parmErrorMessage(retMsg);
break;
}
catch (Exception::Error)
{
sysInfologEnumerator = SysInfologEnumerator::newData(infolog.cut());
while (sysInfologEnumerator.moveNext())
{
sysInfologMessageStruct = new SysInfologMessageStruct(enumerator.currentMessage());
interopException = enumerator.currentException();
retMsg = strfmt("@SYS109465 %1 %2", interopException, sysInfologMessageStruct.message());
ret = checkFailed(retMsg);
}
response.parmSuccess(ret);
response.parmErrorMessage(retMsg);
break;
}
catch (Exception::CLRError)
{
interopException = CLRInterop::getLastException();
ret = checkFailed(interopException.ToString());
retMsg = interopException.ToString();
response.parmSuccess(ret);
response.parmErrorMessage(retMsg);
break;
}
}
}
return response;
}
}I also added a try catch block. If the process throws an error, I am storing the error message in the response object as well.
The Service object, exposes our Service class and allows it to be called by an outside system with the proper authentication. In Visual Studio, right click on your project, and select ‘Add New Item’. Then select ‘Service’.
Give the item a name. I named mine ‘rsmTutServiceSimple’ Open the object. Right click on it and select ‘Properties’
In the Properties window, set the ‘Class’ property with the name of the Service Class you created earlier. Mine is named ‘rsmTutServiceSample’. (The same as the service object). Also, set the ‘External Name’ property to the same value. You can change this to be whatever you want. But it is simpler if you leave it the same.
Now, Right click on the ‘Service Operations’ node, and select ‘New Service Operation’
Select the new node that is created, and in the Properties window set the ‘Method’ property to the name of the method in your Service class. I named mine ‘create’ (you could change this to be something different. But it is simpler if you set it to be the same as the method name). Also set the ‘Name’ property to be the same, set the 'Enable Idempotence' to 'Yes' and set the ‘Subscriber access level’ property to 'Invoke'.
The Service Group is an object that can contain one or more services and is a way of organizing similar operations together.
In Visual Studio right click on the project and select Add>New Item. Select the ‘Service Group’ item. Then give your object a name. I named mine ‘rsmTutServiceGroupSimple’.
Open the new Service Group object, and right click on the main node. Select ‘New Service’ from the drop down.
Select the node that is created, and view the properties window. Set the ‘Name’ and the ‘Service’ properties to the name of your Service object, and set the 'Auto Deploy' to 'Yes'. In my case, the name of my Service object is ‘rsmTutServiceSimple’.
- Open AOT (Application Object Tree) in Visual Studio.
- Navigate to:
Security > Privileges - Create a new Privilege:
- Name it clearly (e.g.,
rsmTutServiceSimple). - Add a description for clarity.
- Name it clearly (e.g.,
- Add Entry Point:
- Right-click the privilege → Add new entry point.
- Select Service Operation as the type.
- Choose the specific service operation (e.g.,
rsmTutServiceSimple).
- Assign Permissions:
- Define access level:
Invoke. - Example: For a read-only service, grant Read permission only.
- Define access level:
- Add the privilege to a Duty, assign that duty to a Role, and then synchronize the database.
- Examples of Permissions
-
Permission (Tables)
- This allows the service operation to access
CustTable.- Reference:
CustTable - Access Level: Read
- Reference:
- This allows the service operation to access
-
Data Entry Permissions (Tables)
- This allows the service operation to update customer records in the
CustTable.- Reference:
CustTable - Access Level: Update
- Reference:
- This allows the service operation to update customer records in the
-
Form Control Permissions
- This ensures that when the service operation is exposed through a form, only permitted controls are accessible.
- Reference:
CustTableForm - Access Level: Read/Update
- Form Control Example:
- Allow access to Customer Name field.
- Restrict access to Credit Limit field.
- Reference:
- This ensures that when the service operation is exposed through a form, only permitted controls are accessible.
-
You can view the previous article for detailed steps on how to use Postman to call the service. In this case the URL to call the service will be:
https://<D365 URL>/api/services/rsmTutServiceGroupSimple/rsmTutServiceSimple/create
-
Let’s break down this URL into it’s parts:
- https:// – This specifies we are making a secure call.
<D365 URL>– This should be replaced by the URL used to access your D365 environment.- For example: https://usnconeboxax1aos.cloud.onebox.dynamics.com
- api/services – This text tells D365 that we are calling a service.
- rsmTutServiceGroupSimple – This is the name of the service group we are calling.
- rsmTutServiceSimple – This is the name of the service we are calling.
- create – This is the name of the method we are calling on the Service class.
-
Consuming the API:
- Get Access Token:
- URL: POST https://login.microsoftonline.com/{YourTenantID}/oauth2/v2.0/token
- Headers: Content-Type: application/x-www-form-urlencoded
- Body (x-www-form-urlencoded):
- client_id: Your Azure AD application's Client ID
- client_secret: Your Azure AD application's Client Secret
- grant_type: client_credentials
- scope: https://usnconeboxax1aos.cloud.onebox.dynamics.com/.default
- Get Access Token:
-
Make Request:
- URL: https://usnconeboxax1aos.cloud.onebox.dynamics.com/api/services/rsmTutServiceGroupSimple/rsmTutServiceSimple/create
- Method: GET (for retrieving data), POST (for creating), PATCH (for updating), DELETE (for deleting)
- Headers: Authorization: Bearer
- Body: For POST/PATCH requests, provide the JSON payload according to the entity's structure.
In this example the Postman body would look like this:
{
"_requestList": {
"requests" : [
{
"DataAreaId": "USRT"
}
]
}
}D365FO – AX – X++ – Converting a Map to a Container
static void MapToContainerExample(Args _args)
{
Map mapExample = new Map(Types::String, Types::Integer);
container conExample = conNull();
mapExample.insert('Apple', 5);
mapExample.insert('Banana', 3);
mapExample.insert('Orange', 2);
// Iterate over the map and convert it into a container
MapEnumerator enumerator = mapExample.getEnumerator();
while (enumerator.moveNext())
{
conExample += [enumerator.currentKey(), enumerator.currentValue()];
}
// Show the container
info(strFmt('Container contents: %1', conExample));
}static void ContainerToMapExample(Args _args)
{
container conExample = ["Apple", 5, "Banana", 3, "Orange", 2];
Map mapExample = null;
int index;
mapExample = new Map(Types::String, Types::Integer);
// Iterate through the container, assuming key-value pairs in odd/even indices
for (index = 1; index <= conLen(conExample) / 2; index++)
{
// Every odd index is a key, and every even index is a value
mapExample.insert(conPeek(conExample, 2 * index - 1), conPeek(conExample, 2 * index));
}
// Retrieve and display the map values
info(strFmt('Apple count: %1', mapExample.lookup('Apple')));
info(strFmt('Banana count: %1', mapExample.lookup('Banana')));
info(strFmt('Orange count: %1', mapExample.lookup('Orange')));
}D365FO – AX – X++ – Check Interactive Session (Client)
static void checkInteractiveClientJob(Args _args)
{
if (Global::hasGUI() == true)
// if (xGlobal::clientKind() == ClientType::Client) // For AX 2012
{
info("This code is running on an interactive client.");
}
else
{
info("This code is not running on an interactive client.");
}
}D365FO – AX – X++ – Cross Company and Change Company Select
static void crossCompanyCaseDetailExampleJob(Args _args)
{
CaseDetailBase caseDetailBase;
while select crossCompany * from caseDetailBase
order by caseDetailBase.RecId desc
where caseDetailBase.CaseId == 'CASE123'
{
if (caseDetailBase)
{
changeCompany(caseDetailBase.dataAreaId)
// changeCompany(curExt())
{
select generateOnly firstOnly * from caseDetailBase
where caseDetailBase.CaseId == 'CASE123';
info(strFmt("Case ID: %1, Company: %2", caseDetailBase.CaseId, caseDetailBase.dataAreaId));
info(caseDetailBase.getSQLStatement());
}
}
}
}D365FO – AX – X++ – Get file name with extension and directory Path
public void fileNameSplit()
{
Filename fileName, fileType, filePath;
[fileName, fileType, filePath] = Docu::splitFilename(@"C:\\Users\\YourUser\\Documents\\example.txt");
System.String fullPath = @"C:\\Users\\YourUser\\Documents\\example.txt";
// Get file name with extension
System.String fileName = System.IO.Path::GetFileName(fullPath);
// Get file name without extension
System.String fileNameWithoutExt = System.IO.Path::GetFileNameWithoutExtension(fullPath);
// Get file extension
System.String fileExtension = System.IO.Path::GetExtension(fullPath);
// Get directory path
System.String directoryPath = System.IO.Path::GetDirectoryName(fullPath);
}D365FO – AX – X++ – Validating SQL Statements Against Injection Risks
-
D365 X++ method that stops SQL injection and blocks update, delete, and similar commands. A general method that can analyze a SQL statement and return a boolean value indicating if it is safe. When executing stored procedures using direct SQL in X++. The Recommended Approach: Use Parameterized Queries.
-
Testing Setup:
DROP PROCEDURE IF EXISTS sp_TM_Test_MyProcCreateReport, sp_TM_Test_MyProc; DROP TABLE IF EXISTS TM_Test_Table;
CREATE TABLE TM_Test_Table (col NVARCHAR(100)); INSERT INTO TM_Test_Table VALUES ('dummy1'), ('dummy2'), ('dummy3');
GO
CREATE PROCEDURE sp_TM_Test_MyProc @param NVARCHAR(50) = 'dummy1' AS BEGIN SELECT T.* FROM (SELECT TOP 1 * FROM TM_Test_Table WHERE col = @param FOR JSON PATH) AS T(RESULT); END;
GO
CREATE PROCEDURE sp_TM_Test_MyProcCreateReport AS BEGIN SELECT T.* FROM (SELECT COUNT(*) AS TotalRows, MAX(col) AS MaxValue FROM TM_Test_Table FOR JSON PATH) AS T(RESULT); END;- Source Code:
class TestSqlInjection
{
public static boolean isSqlStatementSafe(str sqlStatement)
{
// Normalize input
List list;
ListIterator listIterator;
sqlStatement = sqlStatement ? sqlStatement : "";
sqlStatement = strLwr(sqlStatement);
// Mandatory whitelist: only allow stored-proc execution
if (!strStartsWith(sqlStatement, "exec "))
{
return false;
}
// Blacklist of dangerous tokens
list = new List(Types::String);
list.addEnd("create ");
list.addEnd("update ");
list.addEnd("delete ");
list.addEnd("insert ");
list.addEnd("drop ");
list.addEnd("alter ");
list.addEnd("truncate ");
list.addEnd("--");
list.addEnd("/*");
list.addEnd("*/");
// forbiddenTokens.addEnd(";");
// forbiddenTokens.addEnd("xp_");
// forbiddenTokens.addEnd("sp_executesql");
// Scan for each forbidden token
listIterator = new ListIterator(list);
while (listIterator.more())
{
if (strScan(sqlStatement, listIterator.value(), 0, strLen(sqlStatement)) > 0)
{
return false;
}
listIterator.next();
}
return true;
}
public static void testSqlInjection(str testName, str sql, boolean expected)
{
boolean actual = TestSqlInjection::isSqlStatementSafe(sql);
if (actual == expected)
{
info(strFmt('%1: PASS | SQL=`%2`', testName, sql));
}
else
{
error(strFmt('%1: FAIL | SQL=`%2` | expected=%3 got=%4', testName, sql, expected, actual));
}
}
public static void runSqlInjectionTest()
{
TestSqlInjection::testSqlInjection('1. Valid stored proc', "EXEC sp_TM_Test_MyProc @Param=N'dummy1'", true);
TestSqlInjection::testSqlInjection('2. Lowercase exec', 'exec sp_TM_Test_MyProc', true);
TestSqlInjection::testSqlInjection('3. Safe “create” in proc name', 'EXEC sp_TM_Test_MyProcCreateReport', true);
TestSqlInjection::testSqlInjection('4. Missing exec prefix', 'sp_TM_Test_MyProc', false);
TestSqlInjection::testSqlInjection('5. Plain SELECT', 'select * from TM_Test_Table', false);
TestSqlInjection::testSqlInjection('6. Inline comment attack', 'EXEC sp_TM_Test_MyProc -- drop TM_Test_Table', false);
TestSqlInjection::testSqlInjection('7. Block comment attack', 'EXEC sp_TM_Test_MyProc /* malicious */', false);
TestSqlInjection::testSqlInjection('8. DML keyword in body: UPDATE', "EXEC sp_TM_Test_MyProc update TM_Test_Table set Col=N'InjectionTest'", false);
TestSqlInjection::testSqlInjection('9. DML keyword in body: DELETE', 'EXEC sp_TM_Test_MyProc delete from TM_Test_Table', false);
TestSqlInjection::testSqlInjection('10. TRUNCATE in body', 'EXEC sp_TM_Test_MyProc truncate table TM_Test_Table', false);
TestSqlInjection::testSqlInjection('11. DROP in body', 'EXEC sp_TM_Test_MyProc drop table TM_Test_Table', false);
TestSqlInjection::testSqlInjection('12. ALTER in body', 'EXEC sp_TM_Test_MyProc alter table TM_Test_Table add Col bit', false);
TestSqlInjection::testSqlInjection('13. Empty string', '', false);
TestSqlInjection::testSqlInjection('14. Whitespace only', ' ', false);
TestSqlInjection::testSqlInjection('15. Semicolon attack (disabled)', 'EXEC sp_TM_Test_MyProc; DROP TABLE TM_Test_Table', false);
}
public static void main(Args _args)
{
TestSqlInjection::runSqlInjectionTest();
}
}D365FO – AX – X++ – Calling an async .NET method (FetchRef) from X++
public class MyService
{
public async Task<string> FetchRef()
{
// Simulate async work
await Task.Delay(500);
return "Reference_123";
}
}static void main(Args _args)
{
System.Threading.Tasks.Task clrTask;
str result;
// Create instance of the C# class
MyService service = new MyService();
// Call the async method
clrTask = service.FetchRef();
// Wait for completion and get the result
clrTask.Wait(); // Blocks until finished
result = clrTask.get_Result(); // Retrieve the returned string
info(strFmt("Fetched reference: %1", result));
}D365FO – AX – X++ – .NET API browser Dynamics.AX.Application SQL Statement class
static void example()
{
Connection Con;
Statement Stmt;
ResultSet R;
SqlStatementExecutePermission perm;
str sql = 'SELECT VALUE FROM SQLSYSTEMVARIABLES';
Con = new Connection();
Stmt = Con.createStatement();
perm = new SqlStatementExecutePermission(sql);
perm.assert();
// SELECT
R = Stmt.executeQuery(sql);
// R = stmt.executeQueryWithParameters(sql, null);
while ( R.next() )
{
print R.getString(1);
}
// DELETE, INSERT or UPDATE
// Stmt.executeUpdate(sql);
// stmt.executeUpdateWithParameters(sql, null);
CodeAccessPermission::revertAssert();
}D365FO – AX – X++ – Azure App Registration for D365 F&O External API Consumption
To consume D365 Finance & Operations APIs externally (from Postman, Logic Apps, Power Automate, C# applications, etc.), you need to create an Azure App Registration and configure it properly in both Azure Portal and D365 F&O. This enables secure, OAuth 2.0-based authentication for external integrations.
- Navigate to Azure Portal
- Go to Microsoft Entra ID (formerly Azure AD) → App registrations → New registration
- Provide the following details:
- Name: Give it a descriptive name (e.g.,
D365FO-External-API-Client) - Supported account types: Select "Accounts in this organizational directory only"
- Redirect URI: Leave blank for service-to-service authentication
- Name: Give it a descriptive name (e.g.,
- Click Register
- After registration, note down:
- Application (client) ID
- Directory (tenant) ID
- In your App Registration, navigate to Certificates & secrets
- Click New client secret
- Provide a description (e.g.,
D365FO API Secret) - Set an expiration period (recommended: 12-24 months)
- Click Add
- IMPORTANT: Copy the secret value immediately — you won't be able to see it again
- In your App Registration, go to API permissions
- Click Add a permission → Dynamics ERP
- Select Delegated permissions or Application permissions depending on your scenario:
- Delegated: For user-context scenarios
- Application: For service-to-service (recommended for external integrations)
- Check the box for CustomService.FullAccess.All or appropriate permissions
- Click Add permissions
- CRITICAL: Click Grant admin consent for [Your Organization]
- Without this step, authentication will fail
This is the "bridge" that connects your Azure App Registration to D365 F&O's internal security.
- Log into your D365 F&O environment
- Navigate to:
System Administration > Setup > Microsoft Entra ID applications - Click New to add a new record
- Fill in the following:
- Client ID: Paste the Application (client) ID from Azure Portal
- Name: Descriptive name (e.g.,
External API Integration) - User ID: Select a valid D365 user that the integration will run as
- This user's permissions determine what the API can access
- Ensure this user has appropriate security roles assigned
The user associated with the Client ID must be properly configured:
- Navigate to:
System Administration > Users > Users - Search for the User ID you assigned in Step 4
- Verify:
- Enabled: Set to Yes
- Security roles: Ensure appropriate roles are assigned (e.g., Data Management functional user, System administrator, or custom roles)
- Default company: Set to the legal entity the API will primarily access
- Telemetry ID: Should be populated (if blank, see troubleshooting section below)
Use the following approach to get an access token for API calls:
curl -X POST https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=<client-id>&scope=<d365-url>/.default&client_secret=<client-secret>&grant_type=client_credentials"- Create a new request
- Go to Authorization tab
- Select OAuth 2.0
- Configure:
- Grant Type: Client Credentials
- Access Token URL:
https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token - Client ID: Your Application (client) ID
- Client Secret: Your client secret value
- Scope:
https://<your-d365-environment>.operations.dynamics.com/.default- IMPORTANT: Do NOT include a trailing slash
/
- IMPORTANT: Do NOT include a trailing slash
- Click Get New Access Token
public static str getAccessToken()
{
System.Net.WebClient webClient;
System.Collections.Specialized.NameValueCollection values;
System.Text.UTF8Encoding encoding;
System.Byte[] responseBytes;
str responseText;
Newtonsoft.Json.Linq.JObject json;
System.Object accessTokenObj;
str tenantId = "<your-tenant-id>";
str clientId = "<your-client-id>";
str clientSecret = "<your-client-secret>";
str scope = "https://<your-d365-url>/.default";
str tokenEndpoint;
try
{
tokenEndpoint = strFmt("https://login.microsoftonline.com/%1/oauth2/v2.0/token", tenantId);
webClient = new System.Net.WebClient();
values = new System.Collections.Specialized.NameValueCollection();
values.Add("client_id", clientId);
values.Add("scope", scope);
values.Add("client_secret", clientSecret);
values.Add("grant_type", "client_credentials");
responseBytes = webClient.UploadValues(tokenEndpoint, "POST", values);
encoding = new System.Text.UTF8Encoding();
responseText = encoding.GetString(responseBytes);
json = Newtonsoft.Json.Linq.JObject::Parse(responseText);
accessTokenObj = json.get_Item("access_token");
return accessTokenObj.ToString();
}
catch (Exception::CLRError)
{
error(CLRInterop::getLastException().ToString());
return "";
}
}Once you have the access token, you can call D365 F&O OData endpoints:
curl -X GET "https://<your-d365-url>/data/Customers?cross-company=true" \
-H "Authorization: Bearer <access-token>" \
-H "Content-Type: application/json"curl -X POST "https://<your-d365-url>/data/SalesOrders" \
-H "Authorization: Bearer <access-token>" \
-H "Content-Type: application/json" \
-d '{
"dataAreaId": "USMF",
"CustomerAccount": "US-001",
"OrderDate": "2026-02-17T00:00:00Z"
}'This error typically occurs after successfully obtaining a token from Microsoft Entra ID, but before D365 F&O allows the request to access data. Use the following checklist to systematically resolve the issue.
The first point of failure is often the trust relationship between the app registration and the F&O instance.
- Client ID & Secret Accuracy: Ensure the
Client IDandClient Secretused in your request exactly match the App Registration in the Azure Portal. - Resource URL: Verify the
Resource(orAudience) parameter in your token request is exactly your F&O URL (e.g.,https://your-env.operations.dynamics.com).- Note: Do NOT include a trailing slash
/at the end of the URL.
- Note: Do NOT include a trailing slash
- API Permissions: In the App Registration, under API Permissions, ensure you have added Microsoft Dynamics ERP and granted "Delegated" or "Application" permissions (depending on your flow), and importantly, clicked "Grant admin consent for [Company]".
Even if the token is valid, F&O won't recognize it unless the Client ID is mapped to an internal user.
-
Microsoft Entra ID Applications Form:
- Navigate to:
System Administration > Setup > Microsoft Entra ID applications - Verify the Client ID from your App Registration is added here
- Ensure the Name is descriptive and the User ID selected is a valid user in the system
- Navigate to:
-
The Associated User ID:
- Check the
User IDassigned to the Client ID above. This is the "Identity" the integration will assume. - Go to
System Administration > Users > Usersand search for this specific User ID. - Is the user enabled? (Enabled = Yes)
- Does the user have a Telemetry ID? If it is blank, the user might be "invalid."
- Check the
If the user is valid but doesn't have the right to see the data, you may still get vague "invalid user" errors.
- Security Roles: Does the selected User ID have the correct security roles (e.g., Data Management functional user or specific roles for the OData entity)?
- Legal Entity (Company) Access:
- Integration calls often default to the user's "Default company."
- If your API call targets data in
USMFbut the user's default company isDAT, and they don't have access toUSMF, the call may fail. - Fix: Explicitly add
?cross-company=trueto your OData URL or ensure the user has access to the target legal entity.
Starting in recent versions (v10.0.39+), F&O has a dedicated tool for identifying user-related sync issues.
-
Check "Invalid Users" List:
- Go to
System Administration > Users > Invalid users - If your service user appears here, it means their Object ID in Entra ID does not match the Telemetry ID in F&O.
- Fix: Select the user and click Repair telemetry IDs. If that fails, delete the user from F&O and re-import them from Entra ID to refresh the link.
- Go to
-
Maintenance Mode (On-Prem/Dev): If you are on a Dev box and getting this, check if Maintenance Mode is on. Sometimes configuration key changes can temporarily disrupt user validation.
| Checkpoint | Path/Location | Common Culprit |
|---|---|---|
| Token Request | Postman / Integration App | Trailing slash / on Resource URL. |
| Entra ID App | portal.azure.com |
Admin Consent not granted for permissions. |
| Mapping Form | System Admin > Setup > Entra ID apps |
Client ID missing or mapped to the wrong user. |
| User Status | System Admin > Users > Users |
User is disabled or has no roles assigned. |
| ID Sync | System Admin > Users > Invalid users |
Telemetry ID mismatch after a DB refresh. |
D365FO – AX – X++ – Force TLS 1.2, when calling an external HTTPS REST endpoint that requires it
public static void callExternalService()
{
InteropPermission interopPermission;
System.Net.WebHeaderCollection webHeaders;
System.Net.WebClient webClient;
str url;
System.Net.ServicePointManager::set_SecurityProtocol(System.Net.SecurityProtocolType::Tls12);
interopPermission = new InteropPermission(InteropKind::DllInterop);
interopPermission.assert();
try
{
webClient = new System.Net.WebClient();
url = strFmt("https://api.example.com/data");
webHeaders.Add("Authorization", strFmt("Bearer %1", "accessToken"));
webHeaders.Add("x-ms-version", "2025-07-05");
webClient.set_Headers(headers);
webClient.UploadString(url, "DELETE", "");
CodeAccessPermission::revertAssert();
}
catch (Exception::CLRError)
{
error(CLRInterop::getLastException(););
}
}Environmental Documentation
Enabling D365 F&O Extension in Visual Studio for Development
-
Prerequisites
- Visual Studio must be installed before performing the steps below.
-
Steps to Enable D365 F&O Extension
-
Required .NET Frameworks
- .NET Framework 4.5.2 targeting pack
- .NET Framework 4.6.2 targeting pack
- .NET Framework 4.6 targeting pack
-
Step 1: Login to LCS
- Go to Microsoft Dynamics Lifecycle Services (LCS).
- On the main page after login, locate the option for Shared Asset Library.
-
Step 2: Access Software Deployment Package
- In the list of sub-menus, select Software Deployment Package.
- Search for the latest Service Update.
-
Step 3: Download and Locate Installer
- Once downloaded, unzip the folder.
- Navigate to:
ServiceUpdate\DevToolsService\Scripts- Look for the files:
Microsoft.Dynamics.Framework.Tools.Installer.17.0.vsix Microsoft.Dynamics.Framework.Tools.InternalDevTools.17.0.vsix -
Step 4: Install the Extension
-
Follow the wizard to install the
.vsixfile. -
Choose the appropriate version (Visual Studio 2019/2017 or any supported version).
-
This will enable the D365 extension for Finance & Operations.
-
Notes
- Ensure Visual Studio is installed before performing the above steps.
- After successful installation, you will see the extension added to Visual Studio.
-
-
Placeholder
Placeholder







