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

How to create an asynchronous method in D365FO

In d365 for finance and operation a new static method was added to the global class – Global::runAsync()
Let’s create some asynchronous operation:

class SomeClass
{
    public static void main(Args _args)

    {
        SomeClass::runAsync();
    }
}
Ok, Let’s create a table in which the result of each operation will be recorded, a regular table with two fields such as a container and string types. And then launch ten threads for execution. Also, each of the threads created will be written to the list to then check the status of each of them, and after the start of all threads, we will also launch an operation that will check the status of each stream.

public static void runAsync()
{
    System.Threading.Tasks.Task someTask;
    List tasks = new List(Types::Class);
    Counter counter = 0;
    boolean threadsStarting = true;
    for(int i = 0; i < 20; i++)
    {
    // Run Asynchronous task

        someTask = Global::runAsync(
            classNum(SomeClass),
            staticMethodStr(SomeClass, methodRunAsync),
            [‘Async’, int2Str(i)],
            System.Threading.CancellationToken::None,
            classNum(SomeClass),
            staticMethodStr(SomeClass, methodRunAsyncResult));
            tasks.addEnd(someTask);
            counter++;
    }
    //After that, when all task is created, run asynchronous method for
    //check tasks status
    SomeClass::runAfterAllEnded(tasks.pack());
Now let’s simulate our operations, they will be very simple to be easy to understand
CallBack method:

public static void methodRunAsyncResult(AsyncTaskResult _result)
{
    container returnValue;
    SomeTable asyncTable;
    if (_result != null)
    {
        returnValue = _result.getResult();
        str resultstr = conPeek(returnValue, 1);
        Global::info(resultstr);
        ttsbegin;
        asyncTable.FieldString = resultstr;
        asyncTable.FieldContainer = returnValue;
        asyncTable.insert();
        ttscommit;
    }
}
Main method:

public static container methodRunAsync(container _params)
{
    str caller;
    int counter = str2Int(conPeek(_params, 2));
    caller = conPeek(_params, 1);
    caller += ” ” + conPeek(_params, 2);
    System.Threading.Thread::Sleep(1000);
    return([caller]);
}

A method that tracks the status of each stream and, after the execution of the latter, performs some kind of action

public static void runAfterAllEnded(container _params)
{
    boolean threadsRunning = true;
    Counter counter;
    container packedList;
    List tasks = new List(Types::Class);
    for(int i = 3; i < conLen(packedList); i++)
    {
        tasks.addEnd(conPeek(packedList, i+1));
    }

    while (threadsRunning)
    {
        ListEnumerator enumerator = tasks.getEnumerator();
        System.Threading.Tasks.Task currentTask;
        boolean allCompleted = true;

        while(enumerator.moveNext())
        {
            currentTask = enumerator.current();

            if(currentTask.Status == System.Threading.Tasks.TaskStatus::RanToCompletion)
            {
                allCompleted = true && allCompleted;
            }
            else
            {
                allCompleted = false;
                break;
            }
        }
        threadsRunning = !allCompleted;
    }

    SomeTable asyncTable;
    str resultstr = “”;

    while select asyncTable
    {
        str result = “”;

        for (int i = 0; i < conLen(asyncTable.FieldContainer); i++)
        {
            result += conPeek(asyncTable.FieldContainer, i);
        }

        resultstr += result + ” – ” + asyncTable.FieldString + ‘\n’;
    }

    Global::info(resultstr);
}