Tuesday, March 3, 2020

Extend the packed list of standard class variables

This topic shows how to extend the list of packable variables of the existing class which realize SysPackable interface or not, the class needs to have only pack and unpack methods.
For example, you want to extend the SrsPrintDestinationSettings class which have a pack and unpack method, but it did not realize SysPackable interface and add a new print setup for selecting Email Template.
The following example shows how to implement this scenario:

[ExtensionOf(classStr(SrsPrintDestinationSettings))]
public final class MySrsPrintDestinationSettings_Extension 
{
    private SysEmailSystemId    emailTemplateId;

    #define.CurrentVersion(1)
    #localmacro.CurrentList
        emailTemplateId
    #endmacro

    public SrsReportEMailDataContract parmEMailContract(SrsReportEMailDataContract _emailContract)
    {
        next parmEMailContract(_emailContract);

        if(!emailContract.parmBody() && emailTemplateId)
        {
            SysEmailMessageTable sysEmailMessageTable = SysEmailMessageTable::find(emailTemplateId, CompanyInfo::languageId());
            emailContract.parmBody(SysEmailMessage::stringExpand(sysEmailMessageTable.Mail, SysEmailMessage::createStandardPlaceholders()));
        }

        return emailContract;
    }

    [DataMemberAttribute]
    public SysEmailSystemId myParmEmailTemplateId(SysEmailSystemId _emailTemplateId = emailTemplateId)
    {
        emailTemplateId = _emailTemplateId;
        return emailTemplateId;
    }

    public container pack()
    {
        container packedClass = next pack();
        return SysPackExtensions::appendExtension(packedClass, classStr(MySrsPrintDestinationSettings_Extension), this.packMyExtension());
    }

    public boolean unpack(container _packedClass)
    {
        boolean result = next unpack(_packedClass);

        if (result)
        {
            container packedClassExtension = SysPackExtensions::findExtension(_packedClass, classStr(MySrsPrintDestinationSettings_Extension));
            //Also unpack the extension
            if (!this.unpackMyExtension(packedClassExtension))
            {
                result = false;
            }
        }

        return result;
    }

    private container packMyExtension()
    {
        return [#CurrentVersion, #CurrentList];
    }

    private boolean unpackMyExtension(container _packedClass)
    {
        Integer version;

        if (typeOf(conPeek(_packedClass, 1)) == Types::Integer)
        {
            version = conPeek(_packedClass, 1);
        }
        else
        {
            version = RunBase::getVersion(_packedClass);
        }
        
        switch (version)
        {
            case #CurrentVersion:
                [version, #currentList] = _packedClass;
                break;
            default:
                return false;
        }
        return true;
    }

}

To avoid collisions with other eventual extensions, I followed these best practices:

  • Prefix members and methods. In the example, the prefix “my” is used. This practice is important because it helps prevent name clashes with other extensions and future versions of the augmented class.
If you want to extend the RunBase class, docs from MS shows how a RunBase class can be augmented end to end https://docs.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/extensibility/extend-runbase-class

Thursday, August 30, 2018

Register a sales order line or any other inventory transaction


public void registerTransaction()
{
    InventTrans             inventTrans;
    InventDim               inventDim;
    InventTransWMS_Register inventTransWMS_register;
    TmpInventTransWMS       tmpInventTransWMS;

    inventTrans = InventTrans::findTransId(salesLine.InventTransId);
    if(inventTrans && inventTrans.StatusReceipt != StatusReceipt::Registered)
    {
        inventDim   = inventTrans.inventDim();
        tmpInventTransWMS.clear();
        tmpInventTransWMS.initFromInventTrans(inventTrans);
        tmpInventTransWMS.InventQty     = inventTrans.Qty;
        tmpInventTransWMS.InventDimId   = inventDim.inventDimId;
        tmpInventTransWMS.insert();

        inventTransWMS_register = inventTransWMS_register::newStandard(tmpInventTransWMS);
        inventTransWMS_Register.createFromInventTrans(inventTrans, inventDim);
        inventTransWMS_register.writeTmpInventTransWMS(tmpInventTransWMS, 
                                                       inventTrans, inventDim);
        inventTransWMS_register.updateInvent(inventTrans);
    }
}

Create new HCMWorker (Worker)

Example of an x++ method for creating new Worker in AX2012 based on parameters as first name, last name, start date and we need preconfigured default position for hiring a new worker. This piece of code also updates existing worker.

protected void createWorker(){
    DirPersonName           dirPersonName;
    DirPerson               dirPerson;
    HcmWorker               newHcmWorker;
    CompanyInfo             companyInfo;
    HcmPosition             hcmPosition;
    HcmPositionDuration     hcmPositionDuration;
    HcmPositionDetail       hcmPositionDetail,
                            fromHcmPositionDetail;
    FirstName               firstName;
    LastName                lastName;
    ValidFromDateTime       startDate;
    ValidToDateTime         endDate;
    HcmPersonnelNumberId    workerId;
 
    workerId  = 'Worker-01';
    firstName = 'Worker First name';
    firstName = 'Worker Last name';
    startDate = DateTimeUtil::utcNow();
    endDate   = DateTimeUtil::applyTimeZoneOffset(DateTimeUtil::maxValue(), DateTimeUtil::getUserPreferredTimeZone());
 
    ttsBegin;
    dirPersonName.FirstName = firstName;
    dirPersonName.LastName = lastName;
    companyInfo = CompanyInfo::find();

    newHcmWorker = HcmWorker::findByPersonnelNumber(workerId, true);

    //Hire new Worker if Worker not found
    if(!newHcmWorker)
    {
        fromHcmPositionDetail = HcmPositionDetail::findByPosition(11111111);// default position details to creating a new worker
        // Create a position with position details and duration
        hcmPosition.clear();
        hcmPosition.initValue();
        hcmPosition.PositionId = NumberSeq::newGetNum(NumberSeqReference::findReference(extendedTypeNum(HcmPositionId)), true).num();
        hcmPosition.insert();

        buf2Buf(fromHcmPositionDetail, hcmPositionDetail);
        hcmPositionDetail.Position  = hcmPosition.RecId;
        hcmPositionDetail.ValidFrom = startDate;
        hcmPositionDetail.ValidTo   = DateTimeUtil::maxValue();
        hcmPositionDetail.insert();

        hcmPositionDuration.initValue();
        hcmPositionDuration.Position    = hcmPosition.RecId;
        hcmPositionDuration.ValidFrom   = startDate;
        hcmPositionDuration.ValidTo     = DateTimeUtil::maxValue();
        hcmPositionDuration.insert();

        newHcmWorker = HcmWorker::find(HcmWorkerTransition::newHireHcmWorker(   dirPersonName,
                                                                                workerId,
                                                                                hcmPosition.RecId,
                                                                                startDate,
                                                                                endDate,
                                                                                startDate,
                                                                                endDate,
                                                                                companyInfo.RecId,
                                                                                HcmEmploymentType::Employee));
    }
    // Updating an existing worker
    else
    {
        // Updating an existing worker DirPersonName
        if (newHcmWorker)
        {
            dirPersonName = DirPersonName::find(newHcmWorker.Person);

            if(dirPersonName.FirstName != firstName || dirPersonName.LastName != lastName)
            {
                dirPersonName.clear();
                dirPersonName.FirstName = firstName;
                dirPersonName.LastName = lastName;

                dirPerson.initValue();
                dirPerson.updateName(dirPersonName);
                if (dirPerson.validateWrite())
                {
                    dirPerson.insert();

                    dirPersonName.Person = dirPerson.RecId;
                    dirPersonName.ValidFrom = DateTimeUtil::minValue();
                    dirPersonName.ValidTo = DateTimeUtil::maxValue();

                    if (dirPersonName.validateWrite())
                    {
                        dirPersonName.insert();
                    }
                }

                newHcmWorker.Person = dirPerson.RecId;

                if (newHcmWorker.validateWrite())
                {
                    newHcmWorker.update();
                }
            }

            //Update worker assignment
            HcmWorkerTransition::newUpdateHcmEmployment(
                HcmEmployment::findByWorkerLegalEntity(newHcmWorker.RecId, companyInfo.RecId),
                    startDate,
                    endDate);
        }
    }
    ttsCommit;
}

Thursday, May 3, 2018

How to uninstall model from UAT/PROD environment

Recently I need to deleted model, which was installed on the UAT/PROD environment via LCS. So I do not have access to the file system. And all the controls are carried out with the help of the LCS. And there was a need to remove the model from the environment via the LCS.
Please find below the instructions for uninstalling a model in UAT/PROD.

If the model is extension model:
1. Delete all the components from a model and create a deployable package out of it.
2. Create a text file called “ModuleToRemove.txt” and put it into the AOSService\Scripts folder
3. In the text file, put in the name of the module you want to remove, for example – “MyModule”
4. Zip up the package and upload into the asset library
5. Apply the package in a sandbox

If the model is an overlayered model:

1. Remove it (over layered code) from DEV environment in Visual Studio
2. Build the module (say if you have overlayered Application suite, build the Application suite module again after over-layering is removed)
3. Create a deployable package of the module that has over layering removed (say Application suite)
4. Follow the instruction to create a text file in the AOSService\Scripts folder and put Application suite in it.
5. Deploy the package as usual
6. During the deployment, it will wipe the whole Application suite module (with your overlaying) and reapply the new Application suite module in the package (without overlaying)

It should be noted that model directories will remain in the file system, but the models will be removed from the list of models available in the Visual Studio.

Source: Answer from Microsoft Support Engineer

Monday, December 11, 2017

How to import project from one model to another (D365FO)

Hello, Imagine a situation when you have a project that you developed in your own environment in some model. Then you need to import your project into another machine into a certain model (such design rules or something else that forces you to do this).
And then we do not know how to do it, because if we use the standard import method, then our project will load into the source model, and if it does not exist, then it will create it.

There are two ways to get out of this situation:
  • Download the project in a standard way, and then create a project in the model you want and duplicate all the elements from AOT that were in the imported project in your project. Well, then delete the elements and the project that you imported, Build, Sync and rename duplicate elements 
  • Move metadata items.
Imagine what we need to export project1 from "Model 1" to "Model2".
  • Export the project from one system. 
  • Import the project into another system. 
  • Go to the ...AOSService \ PackagesLocalDirectory \ "Model 1" \ folder and move the metadata of the items to the same directory for "Model 2". 
  • Go to the ...AOSService \ PackagesLocalDirectory \ "Model 1" \ XppMetadata \ "Model 1" \ folder and move the metadata of items to a similar directory for "Model 2".
Now you need to set up a new project. Open the VS and create a new project in "Model 2".
  • Go to the folder of the new project and find the .rnrproj file. The same file is found in the folder of the imported project. And then we set up our project, that is, we tied the moved elements to it. 
Example:


<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
    <Configuration>Debug</Configuration>
    <SchemaVersion>2.0</SchemaVersion>
    <Model>Model2</Model>
    <DBSyncInBuild>False</DBSyncInBuild>
    <GenerateFormAdaptors>False</GenerateFormAdaptors>
    <Company>
    </Company>
    <Partition>initial</Partition>
    <PlatformTarget>AnyCPU</PlatformTarget>
    <OutputPath>bin</OutputPath>
    <TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
    <DataEntityExpandParentChildRelations>False</DataEntityExpandParentChildRelations>
    <DataEntityUseLabelTextAsFieldName>False</DataEntityUseLabelTextAsFieldName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
    <DebugSymbols>true</DebugSymbols>
    <EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
  </PropertyGroup>
  <PropertyGroup>
    <ProjectGuid>{YourGUID}</ProjectGuid>
    <Name>ProjectForModel2</Name>
    <RootNamespace>ProjectForModel2</RootNamespace>
    <StartupObject>StartupClass</StartupObject>
    <StartupType>Class</StartupType>
  </PropertyGroup>
  <ItemGroup>
    <Folder Include="Classes\" />
    <Folder Include="Forms\" />
    <Folder Include="Service Groups\" />
    <Folder Include="Services\" />
    <Folder Include="Tables\" />
  </ItemGroup><ItemGroup>
    <Content Include="AxClass\StartupClass">
      <SubType>Content</SubType>
      <Name>StartupClass</Name>
      <Link>Classes\StartupClass</Link>
    </Content>
  </ItemGroup>
  <ItemGroup>
    <Reference Include="Newtonsoft, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
      <Name>Newtonsoft</Name>
      <HintPath>C:\Users\Administrator\Documents\Visual Studio 2015\Projects\Newtonsoft\Newtonsoft\bin\Debug\Newtonsoft.dll</HintPath>
    </Reference>
  </ItemGroup>
  <Import Project="$(MSBuildBinPath)\Microsoft.Common.targets" />
  <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Dynamics\AX\Microsoft.Dynamics.Framework.Tools.BuildTasks.targets" />
</Project>


Usually, in this file, most of the settings are given by the project, the only thing you need to change is to copy the block <ItemGroup><Folder>, <ItemGroup><Content> and <ItemGroup><Reference>. After that, you can delete the imported project.
  • Open your new project in VS. Build and Sync it.That's all. 
I would very appreciate some information about the logical explanation of how the metadata are placed in the file system in D365FO. Thanks

Wednesday, December 6, 2017

Create and run Custom Service in D365FO (AX7) Part 2

In the previous article, I created two services and checked their work.

Let's upgrade the work of our first service in such a way that it could run as many threads as possible, each of which will generate a unique text string, and the service AUTOMATICALLY will call our second service and transmit the generated string. It sounds interesting. Let's get started

I changed the first service in such a way that now it takes as the parameter the service that you want to call, the number of threads, the time of the sleep of each stream. Also, the service now returns a string with the list of thread identifiers it created. I also changed the name of the method, so before you start the service, you need to change the name of the method that is attached to the service operation.

About how to create asynchronous methods in AX7 read on my blog.


class GenerateUniqueString
{
    public str runAsync(str callbackService, int threads, int sleepMS)
    {
        List tasks = new List(Types::Class);
        for(int i = 0; i < threads; i++)
        {
            System.Threading.Tasks.Task task = Global::runAsync(
                    classNum(GenerateUniqueString),
                    staticMethodStr(GenerateUniqueString, methodRunAsync),
                    [callbackService, int2Str(i), sleepMS],
                    System.Threading.CancellationToken::None,
                    classNum(GenerateUniqueString),
                    staticMethodStr(GenerateUniqueString, methodRunAsyncResult));
            tasks.addEnd(task);
        }
        str resultStr = "";
        ListEnumerator enumerator = tasks.getEnumerator();
        while(enumerator.moveNext())
        {
            System.Threading.Tasks.Task current = enumerator.current();
            resultStr += int2Str(current.Id) + ",";
        }

        return resultStr;
    }

    public static container methodRunAsync(container _params)
    {
        str callbackService = conPeek(_params, 1);
        str i = conPeek(_params, 2);
        int sleepMS = conPeek(_params, 3);
        str data = "Test_Async_data_" + i + "_" + datetime2Str(DateTimeUtil::getSystemDateTime());
        
        System.Threading.Thread::Sleep(sleepMS);
        return [callbackService, data];
    }

    public static void methodRunAsyncResult(AsyncTaskResult _result)
    {
        container returnValue = _result.getResult();
        str callbackService, data;
        [callbackService, data] = returnValue;
    }

}

Also, I slightly changed the second service, now it creates a file based on a unique string that came in parameters

class SaveStringToDIsc
{
    public void downloadFile(str parameters)
    {
        TextIo file;
        str fileNameNext = parameters;

        fileNameNext = strReplace(fileNameNext, "/", "_");
        fileNameNext = strReplace(fileNameNext, " ", "_");
        fileNameNext = strReplace(fileNameNext, ":", "_");

        FileName filename = @"C:\Temp\Downloads\" + fileNameNext + ".txt";
        FileIoPermission permission;
        #File
        
        try
        {
            permission = new FileIoPermission(filename, #io_write);
            permission.assert();

            file = new TextIo(filename, #io_write);

            if (!file)
            {
                throw Exception::Error;
            }
            file.write(parameters);
        }
        catch(Exception::Error)
        {
            error("You do not have access to write the file to the selected folder");
        }
        CodeAccessPermission::revertAssert();
    }

}
Now we need to add a request created using the X++ code that we did with the help of Fiddler in the previous article.
At the official GitHub MS account, I found an example of a C# code that allows you to create request and decide to create a small library that can generate requests.
And adding reference it to our X ++ project POST request can be done as follows

public static void methodRunAsyncResult(AsyncTaskResult _result)
    {
        container returnValue = _result.getResult();
        str callbackService, data;
        [callbackService, data] = returnValue;

        OAuthRequestManager.OAuthRequestManager requestManager = new OAuthRequestManager.OAuthRequestManager();
        str contentJson = Newtonsoft.Json.JsonConvert::SerializeObject([data]);
        requestManager.SendPOSTRequest(callbackService, contentJson);
    }

I am creating a new object like OAuthRequestManager - this is my mini library, then I create serialized string JSON type array, for this, I pre-converted my one parameter to the container

The mini-library works in such a way that it is possible to call services with parameters, in which parameters need to be transmitted in the form of a JSON that contains an array of arguments for the service to be called.

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;

namespace OAuthRequestManager
{
    public class OAuthRequestManager
    {
        public string GenerateContentByService(string uri, string jsonList)
        {
            HttpWebResponse response;
            string responseString = string.Empty;

            response = SendGETRequest(uri);

            if (response != null)
            {
                using (Stream responseStream = response.GetResponseStream())
                {
                    using (StreamReader streamReader = new StreamReader(responseStream))
                    {
                        responseString = streamReader.ReadToEnd();
                    }
                }
            }

            GetResponseDataContract contract = JsonConvert.DeserializeObject(responseString);
            var contentObj = JsonConvert.DeserializeObject>(jsonList);
            var resultObj = new Dictionary();

            for (int i = 0; i < contract.Parameters.Count; i++)
            {
                resultObj.Add(contract.Parameters[i].Name, contentObj[i]);
            }

            string result = JsonConvert.SerializeObject(resultObj);

            return result;
        }

        public string GetResponseContent(HttpWebResponse response)
        {
            if (response == null)
            {
                throw new ArgumentNullException("response");
            }

            string responseFromServer = string.Empty;

            using (Stream responseStream = response.GetResponseStream())
            {
                using (StreamReader streamReader = new StreamReader(responseStream))
                {
                    responseFromServer = streamReader.ReadToEnd();
                }
            }

            return responseFromServer;
        }

        public HttpWebResponse SendPOSTRequest(string uri, string jsonList)
        {
            HttpWebRequest request = GeneratePOSTRequest(uri, jsonList);
            return GetResponse(request);
        }

        public HttpWebResponse SendGETRequest(string uri)
        {
            HttpWebRequest request = GenerateGETRequest(uri);
            return GetResponse(request);
        }

        public HttpWebResponse SendRequest(string uri, string jsonList, string method)
        {
            HttpWebRequest request = GenerateRequest(uri, jsonList, method);
            return GetResponse(request);
        }

        public HttpWebRequest GenerateGETRequest(string uri)
        {
            return GenerateRequest(uri, null, "GET");
        }

        public HttpWebRequest GeneratePOSTRequest(string uri, string jsonList)
        {
            return GenerateRequest(uri, jsonList, "POST");
        }

        internal HttpWebRequest GenerateRequest(string uri, string jsonList, string method)
        {
            if (uri == null)
            {
                throw new ArgumentNullException("uri");
            }

            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
            request.Method = method;
            request.Headers[OAuthHelper.OAuthHeader] = OAuthHelper.GetAuthenticationHeader();
 
            if (method == "POST" && !string.IsNullOrEmpty(jsonList))
            {
                var requestContractString = GenerateContentByService(uri, jsonList); 

                using (var stream = request.GetRequestStream())
                {
                    using (var writer = new StreamWriter(stream))
                    {
                        writer.Write(requestContractString);
                    }
                }
            }
            else
            {
                request.ContentLength = 0;
            }

            return request;
        }

        internal HttpWebResponse GetResponse(HttpWebRequest request)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            HttpWebResponse response = (HttpWebResponse)request.GetResponse();

            return response;
        }
    }
}


using System;

namespace OAuthRequestManager
{
    public class ClientConfiguration
    {
        public static ClientConfiguration Default { get { return ClientConfiguration.OneBox; } }

        public static ClientConfiguration OneBox = new ClientConfiguration()
        {
            UriString = "Your Instance Url",
            UserName = "",
            Password = "",
            ActiveDirectoryResource = "Your Instance Url",
            ActiveDirectoryTenant = "https://login.windows.net/Your Tenant/",
            ActiveDirectoryClientAppId = "Your Client Id",
            ActiveDirectoryClientAppSecret = "Your Secret Key",
        };

        public string UriString { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
        public string ActiveDirectoryResource { get; set; }
        public String ActiveDirectoryTenant { get; set; }
        public String ActiveDirectoryClientAppId { get; set; }
        public string ActiveDirectoryClientAppSecret { get; set; }
    }
}


using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace OAuthRequestManager
{
    public class OAuthHelper
    {
        /// 
        /// The header to use for OAuth.
        /// 
        public const string OAuthHeader = "Authorization";

        /// 
        /// Retrieves an authentication header from the service.
        /// 
        /// The authentication header for the Web API call.
        public static string GetAuthenticationHeader()
        {
            string aadTenant = ClientConfiguration.Default.ActiveDirectoryTenant;
            string aadClientAppId = ClientConfiguration.Default.ActiveDirectoryClientAppId;
            string aadResource = ClientConfiguration.Default.ActiveDirectoryResource;

            AuthenticationContext authenticationContext = new AuthenticationContext(aadTenant);
            AuthenticationResult authenticationResult;

            string aadClientAppSecret = ClientConfiguration.Default.ActiveDirectoryClientAppSecret;
            var creadential = new ClientCredential(aadClientAppId, aadClientAppSecret);
            authenticationResult = authenticationContext.AcquireTokenAsync(aadResource, creadential).Result;

            // Create and get JWT token
            return authenticationResult.CreateAuthorizationHeader();
        }
    }
}


using System.Collections.Generic;

namespace OAuthRequestManager
{
    public class GetResponseDataContract
    {
        public IList Parameters { get; set; }
        public GetReposnseType Return { get; set; }
    }
}


namespace OAuthRequestManager
{
    public class GetReposnseType
    {
        public string Name { get; set; }
        public string Type { get; set; }
    }
}


For use this C# code you have to create new C# Class Library with classes which you can see. Then add Nuget Package called Microsoft.Identity.Clients.ActiveDirectory. Then configure ClientConfiguration class. After that build your library adn add to X++ project.

Create and run Custom Service in D365FO (AX7) Part 1

NOTE: Whenever I write a Google query to find information about the new AX system, I mention the bad word of those who create the name for the Microsoft products. Therefore, in order not to write many letters, I will use the name of AX7 instead of Dynamics 365 for Finance and Operation.

In this topic we will create a some two servises, which communicate in async mode.


In our case External System it is our AX services which starting in async mode some operations and each of it operation invoke another AX service which create new file locally.

In the new version of AX, the creation of custom services is a very simple operation. All you need to do is create a service group that will automatically be deployed, create a service and add it to the service group. And finally, link your service with the class which will be responsible for the logic of the service.

Let's create two services, one of which will generate a unique text string asynchronously within itself. In addition, the first service accepts two parameters: the number of threads and the size of the delay for each thread, by this we can emulate some complex task.

1 Step:
Create a service group

And set Auto Deploy properties to Yes
2 Step:
Then create some service

Then add services to Service Group and setup properties



I create Service Group and add two services: 


Ok. Next is to create a class and mapping our service operation with a method on a class. I have seen a situation when we can mapping only instance method and class, so our class and method do not be static.

3 Step:
Create a class


class GenerateUniqueString
{
    public str getString(int sleepMS)
    {
        str data = "Test_Async_data_" + "_" + datetime2Str(DateTimeUtil::getSystemDateTime());
        
        System.Threading.Thread::Sleep(sleepMS);

        return data;
    }

}

and then mapping a class with our service
setting service properties

and also setting service operation properties


Final result for the first service

Now you need to create a second service that will accept a text string and write it to a file and save it to disk. I will give only a piece of code, all other operations on class linking with the service are similar to the first service


class SaveStringToDIsc
{
    public str downloadFile(str parameters)
    {
        TextIo file;
        FileName filename = @"C:\Temp\Downloads\file.txt";
        FileIoPermission permission;
        #File
        str result = "";
        try
        {
            permission = new FileIoPermission(filename, #io_write);
            permission.assert();

            file = new TextIo(filename, #io_write);

            if (!file)
            {
                throw Exception::Error;
            }
            file.write(parameters);
            result = "File was successfully saved";
        }
        catch(Exception::Error)
        {
            result = "You do not have access to write the file to the selected folder";
        }
        CodeAccessPermission::revertAssert();

        return result;
    }

}

Then build your solution for deploy services.

Now let's talk about how to start the services.

NOTE: In this example, I will use services that return and accept data in format JSON. In more detail on how to work with custom services in a different format can be read in the documentation from the Microsoft https://docs.microsoft.com/en-us/dynamics365/unified-operations/dev-itpro/data-entities/custom-services

If you call the GET request, then in response we will receive the signature of our service, but if you run the POST service request, then it will work according to the logic that is written in it. Also, do not forget that the services deployed in the AX instance, so the request must contain a header with an authorization key.

To run the services, I will use the Fiedler program (https://www.telerik.com/download/fiddler)
Let's try to call our services:

Open the program / Composer / Scratchpad


Then we must generate access token with next request:

------------------------- OAUTH

POST https://login.windows.net/Tenant_Id/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: login.windows.net


resource=https%3A%2F%2FYour_Instance_Address&client_id=Your_Client_Id&client_secret=Your_Client_Secret&grant_type=client_credentials

Where
Your_Instance_Address - AX7 url
Your_Client_Id - generated client id
Your_Client_Secret - generated secret key for client id
Tenant_Id - generated tenant id

How generate tenant, client and secret ids see there
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-code

and there
https://www.netiq.com/communities/cool-solutions/creating-application-client-id-client-secret-microsoft-azure-new-portal/

Also, we have to setting client id in AX7, go to System Administration / Setup / Azure Active Directory applications and add new record with your client id.



I recommending use user which has system administrator role
Ok. Lets run our OAuth request, paste your script to scratchpad, select them and click "Execute" button:


Then, found your request and seen response. Copy your access token.



At last, everything is ready to call our service. Let's get started
POST https://YouInstanceUrl/api/services/TestServiceGroup/TestFirstService/getString HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer yourtoken
Host: YouInstanceUrl

{
    "sleepMS":1000
}

Where
yourtoken - token wich we generated and copy previously
Execute this request and see result:


Perfectly. Our service is working.
Now let's do GET request adn see result

GET https://YouInstanceUrl/api/services/TestServiceGroup/TestFirstService/getString HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Authorization: Bearer yourtoken
Host: YouInstanceUrl




GET requests we can do from the browser, for this you just need to enter the address of our service address line (Example: https://YouInstanceUrl/api/services/TestServiceGroup/TestFirstService/getString)

This will complete the first part. In the second part, we will improve our services so that they can call each other automatically