AXForum  
Вернуться   AXForum > Microsoft Dynamics AX > DAX: Программирование
All
Забыли пароль?
Зарегистрироваться Правила Справка Пользователи Сообщения за день Поиск Все разделы прочитаны

 
 
Опции темы Поиск в этой теме Опции просмотра
Старый 07.07.2020, 16:49   #1  
sukhanchik is offline
sukhanchik
Administrator
Аватар для sukhanchik
MCBMSS
Злыдни
Лучший по профессии 2015
Лучший по профессии AXAWARD 2013
Лучший по профессии 2011
Лучший по профессии 2009
 
3,320 / 3547 (125) ++++++++++
Регистрация: 13.06.2004
Адрес: Москва
D365FO: Создание сервиса по шагам
Добрый день! Решил обновить информацию AX 2012 Создание сервиса по шагам но уже для D365FO.

Изменений не так много в части кода на Х++ и радикально много в части кода на C#.
Все проверялось на версии платформы Update35 (7.0.5644.35548) и продукта 10.0.11 (10.0.464.10002)

Итак, задача:
1. Получить по коду клиента его полное наименование и группу
2. Получить по группе клиентов перечень всех клиентов, входящих в эту группу. Карточка клиента в перечне определяется полями:
a. Код клиента (CustTable.AccountNum)
b. Полное наименование (CustTable.name())
c. Группа клиента (CustTable.CustGroup)
Начинаем.
1. Создаем класс Tutorial_CustServiceContract, который будет хранить нашу карточку клиента из трех полей с parm-методами доступа к них (public переменные, объявленные на уровне класса из C# недоступны. Однако в данном примере я сознательно сделал переменные класса public, чтобы из X++ можно было с ними работать напрямую, без использования parm-методов). Класс помечаем атрибутом DataContractAttribute, а методы – DataMemberAttribute.
X++:
/// <summary>
/// Класс-контракт для хранения данных для сервиса по клиентам
/// </summary>

// VSUH, 30.06.2020
[DataContractAttribute]
class Tutorial_CustServiceContract
{
    public CustAccount   custAccount;
    public DirPartyName  custAccountName;
    public CustGroupId   custGroupId;

    [DataMemberAttribute]
    public CustAccount parmCustAccount(CustAccount _custAccount = CustAccount)
    {
        custAccount = _custAccount;
        return custAccount;
    }

    [DataMemberAttribute]
    public DirPartyName parmCustAccountName(DirPartyName _custAccountName = custAccountName)
    {
        custAccountName = _custAccountName;
        return custAccountName;
    }

    [DataMemberAttribute]
    public CustGroupId parmCustGroupId(CustGroupId _custGroupId = custGroupId)
    {
        custGroupId = _custGroupId;
        return custGroupId;
    }

}
2. Создаем класс Tutorial_CustService, который будет вызываться при вызове нашего веб-сервиса. В нем будет 2 метода: getCustDetail для получения карточки клиента по коду клиента и getCustListOfGroup для получения списка карточек клиентов по коду группы клиента. Список оформляется через стандартный класс List, состоящий из экземпляров классов-карточек клиента. Метод getCustListOfGroup помечается атрибутом AifCollectionTypeAttribute, показывающим, что будет возвращена коллекция объектов, которую нужно будет перебирать циклом foreach
X++:
/// <summary>
/// Класс-сервис для вызова снаружи
/// </summary>
 
// VSUH, 30.06.2020
class Tutorial_CustService
{
    /// <summary>
    /// Получение деталей карточки клиента по коду клиента
    /// </summary>
    /// <param name = "_custAccount">
    /// Код клиента
    /// </param>
    /// <returns>
    /// Класс-контракт с деталями карточки клиента
    /// </returns>
    public Tutorial_CustServiceContract getCustDetail(CustAccount _custAccount)
    {
        CustTable                       custTable;
        Tutorial_CustServiceContract    contract;
                
        custTable = CustTable::find(_custAccount);
        contract  = new Tutorial_CustServiceContract();
        contract.custAccount = _custAccount;
        contract.custAccountName = custTable.name();
        contract.custGroupId = custTable.CustGroup;
        return contract;
    }

    /// <summary>
    /// Получение перечня клиентов из заданной группы
    /// </summary>
    /// <param name = "_custGroupId">
    /// Код группы клиентов
    /// </param>
    /// <returns>
    /// Список классов-контрактов с деталями найденных клиентов
    /// </returns>
    [AifCollectionTypeAttribute('return', Types::Class, classstr(Tutorial_CustServiceContract))]
    public List getCustListOfGroup(CustGroupId _custGroupId)
    {
        CustTable                       custTable;
        Tutorial_CustServiceContract    contract;
        List                            contractList;

        contractList = new List(Types::Class);
        while select custTable
            where custTable.CustGroup == _custGroupId
        {
            contract  = new Tutorial_CustServiceContract();
            contract.custAccount = custTable.AccountNum;
            contract.custAccountName = custTable.name();
            contract.custGroupId = custTable.CustGroup;
            contractList.addEnd(contract);
        }
        return contractList;
    }

}
3. Создаем сервис Tutorial_CustService в узле Services и указываем у него свойства:
Class = Tutorial_CustService (связываем сервис с классом Tutorial_CustService)
External Name = Tutorial_LabCustService - под этим именем сервис будет доступен снаружи через SOAP-технологию.
4. Добавляем в сервис операции - наши методы getCustDetail и getCustListOfGroup. У обоих методов включаем свойство Enable Idempotence = Yes (это означает, что при повторном вызове этих методов с теми же параметрами будет выдан тот же результат. Пока не исследовал влияние этого свойства на функциональность работы веб-сервисов). Свойство Subscriber access level отвечает за необходимый доступ к операциям сервиса внешней системы. Я это свойство оставил, как Read по умолчанию, т.к. в моем примере не производится изменение данных. Логично это свойство изменить на Invoke, если веб-сервис предполагает изменение данных в системе.
Название: SNAG_Program-0032.png
Просмотров: 957

Размер: 21.6 Кб

Название: SNAG_Program-0033.png
Просмотров: 953

Размер: 8.9 Кб

Название: SNAG_Program-0034.png
Просмотров: 955

Размер: 9.4 Кб

5. Сервис нужно включить в Service Group, которая будет публиковаться. Поэтому создаем Service Group Tutorial_CustServiceGroup, устанавливаем у нее свойство AutoDeploy = Yes (иначе она не опубликуется) и включаем в эту группу наш сервис Tutorial_CustService
Название: SNAG_Program-0035.png
Просмотров: 953

Размер: 25.0 Кб

Название: SNAG_Program-0036.png
Просмотров: 1199

Размер: 3.8 Кб

6. Делаем билд.
Собственно, всё. Х++-ная часть на этом закончилась .

Веб-сервисы теперь доступны снаружи через технологию SOAP и технологию JSON. Технология SOAP предполагает, что потребитель веб-сервиса получает от него объекты и работает с этими объектами. Технология JSON предполагает, что потребитель веб-сервиса получает некую текстовую информацию, с которой сам разбирается (в т.ч. сам должен создавать у себя необходимые объекты, например - ту же карточку клиента). Поэтому JSON работает быстрее, но сложнее для программиста, а SOAP - удобнее для разработки.

Соответственно SOAP-сервис доступен по адресу https://<URL>/soap/services/Tutorial_CustServiceGroup, где <URL> - это URL нашей системы; на OneBox это usnconeboxax1aos.cloud.onebox.dynamics.com, а Tutorial_CustServiceGroup - название нашей Service Group
Задав в браузере в адресе такую строку мы получаем ответ
Нажмите на изображение для увеличения
Название: SNAG_Program-0037.png
Просмотров: 142
Размер:	71.1 Кб
ID:	12871
Такой ответ говорит нам, что сервис успешно развернут и можно им пользоваться.

JSON-сервис доступен по адресу https://<URL>/api/services/Tutorial_CustServiceGroup, где <URL> - это URL нашей системы; на OneBox это usnconeboxax1aos.cloud.onebox.dynamics.com, а Tutorial_CustServiceGroup - название нашей Service Group
Задав в браузере в адресе такую строку мы получаем ответ
Нажмите на изображение для увеличения
Название: SNAG_Program-0038.png
Просмотров: 155
Размер:	39.8 Кб
ID:	12872
Такой ответ говорит нам, что сервис успешно развернут и можно им пользоваться.

Для JSON можно получить перечень всех веб-сервисов просто по адресу https://<URL>/api/services/, а также можно получить формат данных конкретного сервиса https://<URL>/api/services/Tutorial_CustServiceGroup/Tutorial_CustService и каждой его операции (https://<URL>/api/services/Tutorial_CustServiceGroup/Tutorial_CustService/getCustDetail и https://<URL>/api/services/Tutorial_CustServiceGroup/Tutorial_CustService/getCustListOfGroup)

Официальные (от Microsoft) примеры-заготовки по использованию сервиса через SOAP / JSON расположены по адресу: https://github.com/Microsoft/Dynamic...erviceSamples/
__________________
Возможно сделать все. Вопрос времени

Последний раз редактировалось sukhanchik; 08.07.2020 в 00:16.
За это сообщение автора поблагодарили: raz (10), Logger (10), f18 (2), Stitch_MS (11), IvanS (1).
Старый 07.07.2020, 17:54   #2  
sukhanchik is offline
sukhanchik
Administrator
Аватар для sukhanchik
MCBMSS
Злыдни
Лучший по профессии 2015
Лучший по профессии AXAWARD 2013
Лучший по профессии 2011
Лучший по профессии 2009
 
3,320 / 3547 (125) ++++++++++
Регистрация: 13.06.2004
Адрес: Москва
D365FO: Аутенфикация при использовании веб-сервиса
Независимо от применяемой технологии SOAP / JSON отдельной задачей стоит аутенфикация внешнего приложения при подключении к D365FO.
Для локальной (On premise) версии аутенфикация осуществляется через логин / пароль, которые проверяются службой ADFS (Active Directory Federation Services)
Для облачной (Cloud) версии аутенфикация осуществляется через токен, т.е. большую текстовую строку, которая возвращается в приложение службой аутенфикации. Токен вычисляется на основе 4-х параметров:
  • Tenant: Тенант (а точнее конкретно в данном случае - домен учетной записи пользователя, т.е. если пользователь D365FO, под которым будет обращение к веб-сервису - ассоциирован с учетной записью web-service@ivan-petrov.ru, то в данном случае потребуется значение "ivan-petrov.ru")
  • Resource: Ресурс, т.е. URL системы; для OneBox - это "https://usnconeboxax1aos.cloud.onebox.dynamics.com"
  • AppId: идентификатор приложения в Azure Active Directory (AAD)
  • AppSecret: секретный идентификатор, который генерируется в приложении в AAD

Образец кода для аутенфикации лежит в https://github.com/microsoft/Dynamic...OAuthHelper.cs в методе GetAuthenticationHeader.
X++:
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AuthenticationUtility
{
    public class OAuthHelper
    {
        /// <summary>
        /// The header to use for OAuth authentication.
        /// </summary>
        public const string OAuthHeader = "Authorization";

        /// <summary>
        /// Retrieves an authentication header from the service.
        /// </summary>
        /// <returns>The authentication header for the Web API call.</returns>
        public static string GetAuthenticationHeader(bool useWebAppAuthentication = false)
        {
            string aadTenant = ClientConfiguration.Default.ActiveDirectoryTenant;
            string aadClientAppId = ClientConfiguration.Default.ActiveDirectoryClientAppId;
            string aadClientAppSecret = ClientConfiguration.Default.ActiveDirectoryClientAppSecret;
            string aadResource = ClientConfiguration.Default.ActiveDirectoryResource;

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

            if (useWebAppAuthentication)
            {
                if (string.IsNullOrEmpty(aadClientAppSecret))
                {
                    Console.WriteLine("Please fill AAD application secret in ClientConfiguration if you choose authentication by the application.");
                    throw new Exception("Failed OAuth by empty application secret.");
                }

                try
                {
                    // OAuth through application by application id and application secret.
                    var creadential = new ClientCredential(aadClientAppId, aadClientAppSecret);
                    authenticationResult = authenticationContext.AcquireTokenAsync(aadResource, creadential).Result;
                }
                catch (Exception ex)
                {
                    Console.WriteLine(string.Format("Failed to authenticate with AAD by application with exception {0} and the stack trace {1}", ex.ToString(), ex.StackTrace));
                    throw new Exception("Failed to authenticate with AAD by application.");
                }
            }
            else
            {
                // OAuth through username and password.
                string username = ClientConfiguration.Default.UserName;
                string password = ClientConfiguration.Default.Password;

                if (string.IsNullOrEmpty(password))
                {
                    Console.WriteLine("Please fill user password in ClientConfiguration if you choose authentication by the credential.");
                    throw new Exception("Failed OAuth by empty password.");
                }

                try
                {
                    // Get token object
                    var userCredential = new UserPasswordCredential(username, password); ;
                    authenticationResult = authenticationContext.AcquireTokenAsync(aadResource, aadClientAppId, userCredential).Result;
                }
                catch (Exception ex)
                {
                    Console.WriteLine(string.Format("Failed to authenticate with AAD by the credential with exception {0} and the stack trace {1}", ex.ToString(), ex.StackTrace));
                    throw new Exception("Failed to authenticate with AAD by the credential.");
                }
            }

            // Create and get JWT token
            return authenticationResult.CreateAuthorizationHeader();
        }
    }
}
Его вызов с параметром true авторизует внешнее приложение по токену. Параметр false - по логину и паролю. Параметры подключения прописываются в файле https://github.com/microsoft/Dynamic...nfiguration.cs
X++:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

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

        public static ClientConfiguration OneBox = new ClientConfiguration()
        {
            // You only need to populate this section if you are logging on via a native app. For Service to Service scenarios in which you e.g. use a service principal you don't need that.
            UriString = "https://usnconeboxax1aos.cloud.onebox.dynamics.com/",
            UserName = "tusr1@TAEOfficial.ccsctp.net",            
            // Insert the correct password here for the actual test.
            Password = "",

            // You need this only if you logon via service principal using a client secret. See: [url]https://docs.microsoft.com/en-us/dynamics365/unified-operations/dev-itpro/data-entities/services-home-page[/url] to get more data on how to populate those fields.
            // You can find that under AAD in the azure portal
            ActiveDirectoryResource = "https://usnconeboxax1aos.cloud.onebox.dynamics.com", // Don't have a trailing "/". Note: Some of the sample code handles that issue.
            ActiveDirectoryTenant = "https://login.windows-ppe.net/TAEOfficial.ccsctp.net", // Some samples: [url]https://login.windows.net/yourtenant.onmicrosoft.com[/url], [url]https://login.windows.net/microsoft.com[/url]
            ActiveDirectoryClientAppId = "d8a9a121-b463-41f6-a86c-041272bdb340",
            // Insert here the application secret when authenticate with AAD by the application
            ActiveDirectoryClientAppSecret = "",

            // Change TLS version of HTTP request from the client here
            // Ex: TLSVersion = "1.2"
            // Leave it empty if want to use the default version
            TLSVersion = "",
        };

        public string TLSVersion { get; set; }
        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; }
    }
}
Поскольку OneBox - виртуалка предполагает подключение к AAD, а не к ADFS - я опишу ниже схему настройки AAD для подключения через токен (авторизацию по логину и паролю с ходу у меня не получилось сделать).

Я изначально предполагаю, что имеется виртуалка OneBox, пользователь Admin которой сопоставлен с какой-нибудь учетной записью типа vasya@ivan-petrov.ru и есть возможность (в т.ч. права) под какой-нибудь учетной записью из домена @ivan-petrov.ru сделать настройки в Azure Active Directory.
Итак, заходим на https://portal.azure.com/ и выбираем ярлык Azure Active Directory
Название: SNAG_Program-0039.png
Просмотров: 896

Размер: 9.2 Кб

Слева выбираем пункт Регистрация приложений, жмем кнопку Новая регистрация и указываем имя приложения. Впоследствии мы свяжем данное приложение с пользователем, под которым внешнее приложение будет подключаться к нашему экземпляру системы D365FO, поэтому я указываю в качестве названия приложения - название нашего веб-сервиса
Нажмите на изображение для увеличения
Название: SNAG_Program-0040.png
Просмотров: 108
Размер:	30.1 Кб
ID:	12874
Нажмите на изображение для увеличения
Название: SNAG_Program-0041.png
Просмотров: 98
Размер:	17.2 Кб
ID:	12875
Нажмите на изображение для увеличения
Название: SNAG_Program-0042.png
Просмотров: 94
Размер:	10.3 Кб
ID:	12876
Больше ничего указывать не обязательно, поэтому можно нажать на кнопку Зарегистрировать.
После этого у нас откроется страничка, где уже будет нашему приложению присвоен Идентификатор приложения, он же Application Id (AppId), который мы впоследствии укажем в "четверке" параметров подключения в поле AppId
Нажмите на изображение для увеличения
Название: SNAG_Program-0043.png
Просмотров: 98
Размер:	17.6 Кб
ID:	12877

Далее этому приложению нужно добавить секретный идентификатор, тот - который у нас будет указан в поле AppSecretId. Для этого, не уходя со странички приложения выбираем слева пункт "Сертификаты и секреты"
Название: SNAG_Program-0044.png
Просмотров: 894

Размер: 23.6 Кб
И нажимаем кнопку "Новый секрет клиента"
Нажмите на изображение для увеличения
Название: SNAG_Program-0045.png
Просмотров: 97
Размер:	76.3 Кб
ID:	12879
Указываем описание (любое) и срок действия - 1, 2 года или бессрочно и жмем кнопку Добавить
Название: SNAG_Program-0046.png
Просмотров: 898

Размер: 13.2 Кб
Мы получаем секретный идентификатор, который впоследствии и нужно будет подставить в поле AppSecret. Его желательно сразу сохранить, т.к. потом на этой странице он уже отображаться не будет и его придется заново генерировать (в то время, как идентификатор приложения можно будет увидеть при повторном открытии страницы).
Нажмите на изображение для увеличения
Название: SNAG_Program-0047.png
Просмотров: 87
Размер:	10.1 Кб
ID:	12881
Теперь нам нужно ассоциировать наше приложение с пользователем в D365FO. Для этого нужно зайти в систему в модуль Администрирование и выбрать пункт меню Настройка - Приложения Azure Active Directory
Нажмите на изображение для увеличения
Название: SNAG_Program-0048.png
Просмотров: 108
Размер:	44.6 Кб
ID:	12882
И настроить соответствие между идентификатором приложения (AppId, код клиента) и пользователем системы (пользователь может и должен быть ограничен в правах)
Нажмите на изображение для увеличения
Название: SNAG_Program-0049.png
Просмотров: 102
Размер:	51.5 Кб
ID:	12883

Дело осталось за малым - подготовить проект на C#, используя заготовки Microsoft и полученные идентификаторы AppId и AppSecret.
На всякий случай отмечу, что в данном примере у меня получились следующие значения:
Цитата:
AppId: "d4a93f6d-1d46-4e33-a258-3aa3ae7cb819"
AppSecretId: "J6Z86h8.JhXxYH.43TM5Bap7~_UZ29wI_R"
Tenant: "ivan-petrov.ru"
Resource: "https://usnconeboxax1aos.cloud.onebox.dynamics.com"
Итоговый файл ClientConfiguration у нас получится такой:
X++:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

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

        public static ClientConfiguration OneBox = new ClientConfiguration()
        {
            // You only need to populate this section if you are logging on via a native app. For Service to Service scenarios in which you e.g. use a service principal you don't need that.
            UriString = "",
            UserName = "",
            // Insert the correct password here for the actual test.
            Password = "",

            // You need this only if you logon via service principal using a client secret. See: [url]https://docs.microsoft.com/en-us/dynamics365/unified-operations/dev-itpro/data-entities/services-home-page[/url] to get more data on how to populate those fields.
            // You can find that under AAD in the azure portal
            ActiveDirectoryResource = "https://usnconeboxax1aos.cloud.onebox.dynamics.com", // Don't have a trailing "/". Note: Some of the sample code handles that issue.
            ActiveDirectoryTenant = "https://login.windows.net/ivan-petrov.ru", // Some samples: [url]https://login.windows.net/yourtenant.onmicrosoft.com[/url], [url]https://login.windows.net/microsoft.com[/url]
            ActiveDirectoryClientAppId = "d4a93f6d-1d46-4e33-a258-3aa3ae7cb819",
            // Insert here the application secret when authenticate with AAD by the application
            ActiveDirectoryClientAppSecret = "J6Z86h8.JhXxYH.43TM5Bap7~_UZ29wI_R",

            // Change TLS version of HTTP request from the client here
            // Ex: TLSVersion = "1.2"
            // Leave it empty if want to use the default version
            TLSVersion = "",
        };

        public string TLSVersion { get; set; }
        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; }
    }
}
Он отличается от оригинального:
- Отсутствием значений переменных UserName, Password и UriString
- Заполненными значениями переменных ActiveDirectoryResource (URL нашей системы D365FO), ActiveDirectoryTenant (строка "https://login.windows.net/", к которой добавлен наш домен ivan-petrov.ru), ActiveDirectoryClientAppId и ActiveDirectoryClientAppSecret, полученных при регистрации приложения в Azure AD
__________________
Возможно сделать все. Вопрос времени

Последний раз редактировалось sukhanchik; 08.07.2020 в 00:03.
Старый 07.07.2020, 22:31   #3  
sukhanchik is offline
sukhanchik
Administrator
Аватар для sukhanchik
MCBMSS
Злыдни
Лучший по профессии 2015
Лучший по профессии AXAWARD 2013
Лучший по профессии 2011
Лучший по профессии 2009
 
3,320 / 3547 (125) ++++++++++
Регистрация: 13.06.2004
Адрес: Москва
D365FO: Использование веб-сервиса через SOAP
В качестве образца использования веб-сервиса можно взять файл https://github.com/microsoft/Dynamic...ion/Program.cs, однако в конечном счете все нужно будет собрать в один проект на C#, поэтому начинаем с того, что создаем проект на C# типа Console Application. Мы можем создать проект любого типа - главное, чтобы он мог бы быть напрямую запущен пользователем.
Нажмите на изображение для увеличения
Название: SNAG_Program-0050.png
Просмотров: 102
Размер:	26.5 Кб
ID:	12884
В созданном проекте для авторизации по токену будет не хватать ссылки на библиотеку Microsoft.IdentityModel.Clients.ActiveDirectory (см узел References)
Название: SNAG_Program-0051.png
Просмотров: 891

Размер: 15.7 Кб
Поэтому эту библиотеку нужно добавить путем выполнения команды в Visual Studio: Tools - NuGet Package Manager - Package Manager Console
Нажмите на изображение для увеличения
Название: SNAG_Program-0052.png
Просмотров: 94
Размер:	24.5 Кб
ID:	12886
Цитата:
Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory
Выполнять команду в консоли стоит, щелкнув предварительно на Solution, а не на Project

Теперь в проект нужно добавить из заготовок от Microsoft следующие файлы:
ClientConfiguration.cs (с правками по параметрам авторизации)
OAuthHelper.cs (без правок)
SoapHelper.cs (без правок)
Также добавляем Service Reference
Нажмите на изображение для увеличения
Название: SNAG_Program-0053.png
Просмотров: 103
Размер:	34.0 Кб
ID:	12887
указав путь к веб-сервису (и нажав кнопку Go), а также указав имя Tutorial_CustServiceReference, под которым веб-сервис будет представлен в коде
Нажмите на изображение для увеличения
Название: SNAG_Program-0054.png
Просмотров: 103
Размер:	22.5 Кб
ID:	12888

Далее пишем вот такой вот класс Program.cs. Обращаю внимание, что parm-методы воспринимаются на C#, как свойства (property) класса, а перечень карточек клиента - как массив.
X++:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using AuthenticationUtility;
using SoapUtility;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace Tutorial_ConsumeServiceSOAP
{
    class Program
    {
        public const string serviceGroupName = "Tutorial_CustServiceGroup";
        public const string serviceName = "Tutorial_LabCustService";
        static void Main(string[] args)
        {
            var aosUriString = ClientConfiguration.Default.ActiveDirectoryResource;
            string bearerKey;

            bearerKey = OAuthHelper.GetAuthenticationHeader(true);

            var serviceUriString = SoapUtility.SoapHelper.GetSoapServiceUriString(serviceGroupName, aosUriString);

            EndpointAddress endpointAddress = new System.ServiceModel.EndpointAddress(serviceUriString);
            var binding = SoapUtility.SoapHelper.GetBinding();

            var client = new Tutorial_CustServiceReference.Tutorial_LabCustServiceClient(binding, endpointAddress);
            var channel = client.InnerChannel;

            Tutorial_CustServiceReference.CallContext refContext = new Tutorial_CustServiceReference.CallContext();
            refContext.Company = "RUMF";
            refContext.Language = "ru";
            refContext.PartitionKey = "initial";

            using (OperationContextScope operationContextScope = new OperationContextScope(channel))
            {
                HttpRequestMessageProperty requestMessage = new HttpRequestMessageProperty();
                requestMessage.Headers[OAuthHelper.OAuthHeader] = bearerKey;
                OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = requestMessage;
                Tutorial_CustServiceReference.Tutorial_CustServiceContract result;
                Tutorial_CustServiceReference.Tutorial_CustServiceContract[] colResult;
                result = ((Tutorial_CustServiceReference.Tutorial_LabCustService)channel).getCustDetail(new Tutorial_CustServiceReference.getCustDetail(refContext, "RUMF-000001")).result;
                
                Console.WriteLine(string.Format("{0} | {1} | {2}", result.parmCustAccount, result.parmCustAccountName, result.parmCustGroupId));

                colResult = ((Tutorial_CustServiceReference.Tutorial_LabCustService)channel).getCustListOfGroup(new Tutorial_CustServiceReference.getCustListOfGroup(refContext, "Орг")).result;
                foreach (Tutorial_CustServiceReference.Tutorial_CustServiceContract iterator in colResult)
                {
                    Console.WriteLine(string.Format("{0} | {1} | {2}", iterator.parmCustAccount, iterator.parmCustAccountName, iterator.parmCustGroupId));
                }
            }
            Console.ReadLine();
        }
    }
}
Все билдим, запускаем, проверяем. Само собой, база данных должна содержать те данные, которые мы хотим получить.
У меня (на моей базе) получается такой результат:
Название: Снимок.PNG
Просмотров: 595

Размер: 24.8 Кб
__________________
Возможно сделать все. Вопрос времени

Последний раз редактировалось sukhanchik; 09.12.2020 в 08:55.
Старый 07.07.2020, 23:17   #4  
sukhanchik is offline
sukhanchik
Administrator
Аватар для sukhanchik
MCBMSS
Злыдни
Лучший по профессии 2015
Лучший по профессии AXAWARD 2013
Лучший по профессии 2011
Лучший по профессии 2009
 
3,320 / 3547 (125) ++++++++++
Регистрация: 13.06.2004
Адрес: Москва
D365FO: Использование веб-сервиса через JSON
Для JSON структура построения кода несколько иная. Нам необходимо подготовить строку текста в некотором формате, затем вызвать сервис, передав ему эту строку, после чего получить ответ также в виде строки и разобрать полученную строку.
Тем не менее, нам также нужно создать проект на C# типа Console Application (или любого типа, который может быть запущен напрямую пользователем)
Процедура аутенфикации такая же, поэтому также необходимо добавлять в проект ссылку на библиотеку Microsoft.IdentityModel.Clients.ActiveDirectory путем выполнения команды
Цитата:
Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory
в Package Manager Console
Строку можно готовить (сериализовать) и разбирать (десериализовать) как "ручками", так и при помощи библиотеки Newtonsoft.Json, которую аналогичным способом (как и Microsoft.IdentityModel.Clients.ActiveDirectory) можно добавить в Package Manager Console путем выполнения команды
Цитата:
Install-Package Newtonsoft.Json
Также в проект необходимо добавить из заготовок от Microsoft следующие файлы:
ClientConfiguration.cs (с правками по параметрам авторизации)
OAuthHelper.cs (без правок)

А теперь начинаются различия.
Для начала нам понадобится класс JsonUtil, который будет генерировать нам URL-адрес, добавляя в него группу сервисов, сам сервис и операции сервиса.
X++:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Tutorial_ConsumeServiceJSON
{
    public class JsonUtil
    {
        public const string OAuthHeader = "Authorization";
        public static string GetJsonServiceGroupURL(string aosUriString, string serviceGroupName)
        {
            var serviceUriStringTemplate = "{0}/api/services/{1}";
            var serviceUriString = string.Format(serviceUriStringTemplate, aosUriString.TrimEnd('/'), serviceGroupName);
            return serviceUriString;
        }
        public static string GetJsonServiceNameURL(string aosUriString, string serviceGroupName, string serviceName)
        {
            var serviceUriStringTemplate = "{0}/{1}";
            var serviceUriString = string.Format(serviceUriStringTemplate, GetJsonServiceGroupURL(aosUriString, serviceGroupName), serviceName);
            return serviceUriString;
        }
        public static string GetJsonOperationURL(string aosUriString, string serviceGroupName, string serviceName, string functionName)
        {
            var serviceUriStringTemplate = "{0}/{1}";
            var serviceUriString = string.Format(serviceUriStringTemplate, GetJsonServiceNameURL(aosUriString, serviceGroupName, serviceName), functionName);
            return serviceUriString;
        }
    }
}
Далее, нам нужно:
1. Описать класс-контракт, описанный на X++, т.к. в отличие от SOAP - здесь не передаются объекты, а значит для получения карточки клиента - нам потребуется повторное описание полей карточки.
Это делается так:
X++:
    public class Tutorial_CustServiceContract
    {
        public string parmCustAccount { get; set; }
        [JsonProperty("parmCustAccountName")]
        public string custAccountName { get; set; }
        public string parmCustGroupId { get; set; }
    }
Здесь при помощи атрибута JsonProperty мы можем определять названия переменных, которые не соответствуют названиям переменных, приходящих из сервиса (из сервиса нам приходят parmCustAccountName, а мы не обязаны использовать внутри C# именно такое название. Зато если мы используем именно такое название, то мы не обязаны использовать атрибут JsonProperty)

2. Библиотека Newtonsoft.Json умеет сериализовать / десериализовать только классы, поэтому если нам надо передать в сервис обычное значение (например, код клиента), то чтобы вручную не формировать строку JSON - нужно создать классы, состоящие из одной этой переменной:
X++:
    public class ParmCustAccount
    {
        [JsonProperty("_custAccount")]
        public string custAccount { get; set; }
    }
    public class ParmCustGroupId
    {
        [JsonProperty("_custGroupId")]
        public string custGroupId { get; set; }
    }
3. При передаче строки JSON в сервис - необходимо устанавливать свойство SendChunked в true для корректной обработки символов в Юникоде.

В итоге у меня получилось 2 класса:
JsonServices, который вызывает веб-сервис
X++:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.IO;
using Newtonsoft.Json;
using AuthenticationUtility;

namespace Tutorial_ConsumeServiceJSON
{
    public class Tutorial_CustServiceContract
    {
        public string parmCustAccount { get; set; }
        [JsonProperty("parmCustAccountName")]
        public string custAccountName { get; set; }
        public string parmCustGroupId { get; set; }
    }
    public class ParmCustAccount
    {
        [JsonProperty("_custAccount")]
        public string custAccount { get; set; }
    }
    public class ParmCustGroupId
    {
        [JsonProperty("_custGroupId")]
        public string custGroupId { get; set; }
    }
    public class JsonServices
    {
        public const string serviceGroupName = "Tutorial_CustServiceGroup";
        public const string serviceName = "Tutorial_CustService";
        public const string getCustDetailName = "getCustDetail";
        public const string getCustListOfGroupName = "getCustListOfGroup";

        string aosUriString;
        string bearerKey;
        [JsonProperty("return")]
        public Tutorial_CustServiceContract[] serviceContracts { get; set; }
        [JsonProperty("return")]
        public Tutorial_CustServiceContract serviceContract { get; set; }
        public JsonServices()
        {
            aosUriString = ClientConfiguration.Default.ActiveDirectoryResource;
            bearerKey = OAuthHelper.GetAuthenticationHeader(true);
        }
        private HttpWebRequest CreateRequest(string _address)
        {
            HttpWebRequest webRequest;
            webRequest = (HttpWebRequest)HttpWebRequest.Create(_address);
            webRequest.Method = "POST";
            // the request will be empty.
            webRequest.ContentLength = 0;
            webRequest.Headers.Set(JsonUtil.OAuthHeader, bearerKey);
            return webRequest;
        }
        private string ReadJsonResponse(HttpWebRequest _request)
        {
            string jsonString;
            using (HttpWebResponse webResponse = (HttpWebResponse)_request.GetResponse())
            {
                using (Stream stream = webResponse.GetResponseStream())
                {
                    using (StreamReader reader = new StreamReader(stream))
                    {
                        jsonString = reader.ReadToEnd();
                    }
                }
            }
            return jsonString;
        }
        public Tutorial_CustServiceContract getCustDetail(string _custAccount)
        {
            string operationURL = JsonUtil.GetJsonOperationURL(aosUriString, serviceGroupName, serviceName, getCustDetailName);

            ParmCustAccount parmCustAccount = new ParmCustAccount();
            parmCustAccount.custAccount = _custAccount;
            string jsonCustAccount = JsonConvert.SerializeObject(parmCustAccount);
            HttpWebRequest webRequest;
            webRequest = CreateRequest(operationURL);
            webRequest.SendChunked = true;
            using (Stream stream = webRequest.GetRequestStream())
            {
                using (StreamWriter writer = new StreamWriter(stream))
                {
                    writer.Write(jsonCustAccount);
                    writer.Flush();
                }
            }
            string jsonResponse = ReadJsonResponse(webRequest);
            serviceContract = JsonConvert.DeserializeObject<Tutorial_CustServiceContract>(jsonResponse);
            return serviceContract;
        }
        public Tutorial_CustServiceContract[] getCustListOfGroup(string _custGroupId)
        {
            string operationURL = JsonUtil.GetJsonOperationURL(aosUriString, serviceGroupName, serviceName, getCustListOfGroupName);

            ParmCustGroupId parmCustGroupId = new ParmCustGroupId();
            parmCustGroupId.custGroupId = _custGroupId;
            string jsonCustGroupId = JsonConvert.SerializeObject(parmCustGroupId);
            HttpWebRequest webRequest;
            webRequest = CreateRequest(operationURL);
            webRequest.SendChunked = true;
            using (Stream stream = webRequest.GetRequestStream())
            {
                using (StreamWriter writer = new StreamWriter(stream))
                {
                    writer.Write(jsonCustGroupId);
                    writer.Flush();
                }
            }
            string jsonResponse = ReadJsonResponse(webRequest);
            serviceContracts = JsonConvert.DeserializeObject<Tutorial_CustServiceContract[]>(jsonResponse);
            return serviceContracts;
        }
    }
}
Program, который вызывает JsonServices
X++:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.IO;
using AuthenticationUtility;

namespace Tutorial_ConsumeServiceJSON
{
    class Program
    {
        static void Main(string[] args)
        {
            JsonServices jsonServices = new JsonServices();
            Tutorial_CustServiceContract contract;
            Tutorial_CustServiceContract[] contracts;

            contract = jsonServices.getCustDetail("RUMF-000001");
            Console.WriteLine(string.Format("{0} | {1} | {2}", contract.parmCustAccount, contract.custAccountName, contract.parmCustGroupId));
            contracts = jsonServices.getCustListOfGroup("Орг");
            foreach (Tutorial_CustServiceContract iterator in contracts)
            {
                Console.WriteLine(string.Format("{0} | {1} | {2}", iterator.parmCustAccount, iterator.custAccountName, iterator.parmCustGroupId));
            }
            Console.ReadLine();
        }
    }
}
Соответственно, билдим, запускаем, смотрим.
Результат, как и в случае с SOAP должен быть таким же
Название: Снимок.PNG
Просмотров: 601

Размер: 24.8 Кб
__________________
Возможно сделать все. Вопрос времени

Последний раз редактировалось sukhanchik; 09.12.2020 в 08:56.
Старый 07.07.2020, 23:59   #5  
sukhanchik is offline
sukhanchik
Administrator
Аватар для sukhanchik
MCBMSS
Злыдни
Лучший по профессии 2015
Лучший по профессии AXAWARD 2013
Лучший по профессии 2011
Лучший по профессии 2009
 
3,320 / 3547 (125) ++++++++++
Регистрация: 13.06.2004
Адрес: Москва
Отдельно прикреплю проект. Все делалось в одном Solution.
Также хочу отметить, что домент @ivan-petrov.ru выдуманный, а созданное приложение в AAD уже удалено, поэтому в чистом виде данный код в режиме "скопировал и работает" работать не будет. Нужно будет подставить реальный домен, и реальные идентификаторы приложения и секрета. Ну и само собой данные должны быть в базе данных в соответствии с примером.

10.07.2020
Обновил архив и сообщения, т.к. как выяснилось - закралась ошибка. В строке, где инициализировалась переменная aosUriString - она бралась из файла ClientConfiguration, в котором переменная UriString была пустой. Исправил на ActiveDirectoryResource (т.е. переменная aosUriString теперь инициализируется из файла ClientConfiguration из переменной ActiveDirectoryResource).
Исправление коснулось как SOAP, так и JSON. Поэтому архив перевыложил. EXE-шники от C# не перебилдивал - их все равно нужно будет перебилдивать после изменения файла ClientConfiguration на реальные значения идентификаторов
Вложения
Тип файла: zip Tutorial_LabService.zip (2.61 Мб, 100 просмотров)
__________________
Возможно сделать все. Вопрос времени

Последний раз редактировалось sukhanchik; 10.07.2020 в 20:28.
За это сообщение автора поблагодарили: trud (20), Manner (1), AvrDen (1), Weez (3), Jorj (1), imir (2), Pandasama (1).
 

Похожие темы
Тема Автор Раздел Ответов Посл. сообщение
AX 2012 Создание сервиса по шагам sukhanchik DAX: Программирование 16 26.08.2021 12:26
littleax: Simple Rest API in D365FO, D365F, D365SCM Blog bot DAX Blogs 0 23.06.2020 14:12
sertandev: How to receive D365FO push notifications using Azure Notification Hubs Blog bot DAX Blogs 0 04.07.2019 18:11
sertandev: How to integrate D365FO with Microsoft Flow using the new Business Events Blog bot DAX Blogs 0 23.05.2019 16:11
erconsult: Copy-paste with keyboard script 2: from Excel to D365FO Blog bot DAX Blogs 0 03.08.2018 11:12
Опции темы Поиск в этой теме
Поиск в этой теме:

Расширенный поиск
Опции просмотра

Ваши права в разделе
Вы не можете создавать новые темы
Вы не можете отвечать в темах
Вы не можете прикреплять вложения
Вы не можете редактировать свои сообщения

BB коды Вкл.
Смайлы Вкл.
[IMG] код Вкл.
HTML код Выкл.
Быстрый переход

Рейтинг@Mail.ru
Часовой пояс GMT +3, время: 05:50.