Dynamics 365 – Provedor de Dados Customizado/Custom Data Provider (Parte 2/3)


Olá pessoal,

Dando continuidade ao tema Provedor de Dados Customizado, utilizado para consumir APIs Externas do Dynamics 365, vamos a parte 2!

Para aqueles que ainda não viram a primeira parte, por favor, acessem a aqui!

Vamos lá…

Provedor de Dados Customizado (código)

Vamos ao código do provedor de dados, mesmo não sendo um grande desenvolvedor C#, tentei dividir as funcionalidades em algumas classes para facilitar o reuso. Então se assustem com a quantidade, as classes são pequenas.

O printscreen to projeto no Visual Studio para ajudar a se localizar:

Primeiro vamos as classes auxiliares(Helpers)…

O Helper.cs é a espinha dorsal, possui ométodo GetAPIData é o “cara”! Responsável pelas chamadas à API Externa e de recuperar a resposta.

using Microsoft.Xrm.Sdk;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Runtime.Serialization.Json;
using System.Threading.Tasks;

namespace MyCustomDataProvider
{
    public class Helper
    {
        public static async Task<string> GetAPIData(ITracingService tracer, string url)
        {
            using (HttpClient client = HttpHelper.GetHttpClient())
            {
                tracer.Trace($"API URL: {url}");

                HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
                HttpResponseMessage response = await client.SendAsync(request);

                // If response status is NOT Sucess
                if (!response.IsSuccessStatusCode)
                    throw new Exception("API has stopped this from happening");

                string json = response.Content.ReadAsStringAsync().Result;

                tracer.Trace($"JSON response: {json}");

                return json;
            }
        }

        public static List<T> DeserializeJSONObject<T>(string json)
        {
            return DeserializeObject<List<T>>(json);
        }

    public static T DeserializeObject<T>(string json)
        {
            using (MemoryStream stream = new MemoryStream())
            {
                DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
    StreamWriter writer = new StreamWriter(stream);
    writer.Write(json);
                writer.Flush();
                stream.Position = 0;
                T responseObject = (T)serializer.ReadObject(stream);
                return responseObject;
            }
        }

        public static string SerializeObject<T>(object obj)
        {
            using (MemoryStream stream = new MemoryStream())
            {
                DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
serializer.WriteObject(stream, obj);
                stream.Position = 0;
                StreamReader reader = new StreamReader(stream);
string requestBody = reader.ReadToEnd();
                return requestBody;
            }
        }
    }
}

A classeHttpHelper.cs, apenas criar o objeto HttpClient:

using System.Net.Http;

namespace MyCustomDataProvider
{
    public class HttpHelper
    {
        public static HttpClient GetHttpClient()
        {
            HttpClient client = new HttpClient();
            client.DefaultRequestHeaders.Add("Connection", "close");

            return client;
        }
    }
}

A classeSearchVisitor.cs, implementa a interface IQueryExpressionVisitor, obrigando o override do método Visit, com isso conseguimos recuperar o que está sendo consultado, ou seja, os critérios(criterias) da consulta:

using Microsoft.Xrm.Sdk.Query;

namespace MyCustomDataProvider
{
    public class SearchVisitor : IQueryExpressionVisitor
    {
        public string SearchKeyWord { get; private set; }

        public QueryExpression Visit(QueryExpression query)
        {
            // Returning null will get a random result
            if (query.Criteria.Conditions.Count == 0)
                return null;

            // Get the first filter value
            SearchKeyWord = query.Criteria.Conditions[0].Values[0].ToString();

            return query;
        }
    }
}

Criei a classe ExternalActivity.cs como uma espécie de Model e Controller ao mesmo tempo, poderia ter separado, mas deixei assim…

using Microsoft.Xrm.Sdk;
using System;
using System.Collections.Generic;

namespace MyCustomDataProvider
{
    public class ExternalActivity
    {
        #region Properties
        public string ExternalActivityId { get; set; }

        public int ExternalActivityKey { get; set; }

        public string ContactId { get; set; }

        public string Subject { get; set; }

        public string Details { get; set; }

        public string From { get; set; }

        public string To { get; set; }
        #endregion

        public static EntityCollection FillEntityCollection(ITracingService tracer, string json)
        {
            EntityCollection results = new EntityCollection();

            // Deserialize JSON object to List of External Activities (List<ExternalActivity>)
            List<ExternalActivity> listExternalActivity = Helper.DeserializeJSONObject<ExternalActivity>(json);

            tracer.Trace($"Results Count: {listExternalActivity.Count.ToString()}");

            // Loop API results
            foreach (ExternalActivity externalActivity in listExternalActivity)
            {
                Entity entity = CreateExternalActivity(externalActivity);

                if (entity == null)
                    continue;

                results.Entities.Add(entity);
            }

            return results;
        }

        private static Entity CreateExternalActivity(ExternalActivity externalActivity)
        {
            // Check if primary field has informed
            if (string.IsNullOrEmpty(externalActivity.ExternalActivityId))
                return null;

            return new Entity("new_externalentity")
            {
                ["new_externalentityid"] = new Guid(externalActivity.ExternalActivityId),
                ["new_contactid"] = new EntityReference("contact", new Guid(externalActivity.ContactId)),
                ["new_subject"] = externalActivity.Subject,
                ["new_from"] = externalActivity.From,
                ["new_to"] = externalActivity.To,
                ["new_details"] = externalActivity.Details,
            };
        }
    }
}

FillEntityCollection – é o método que realiza a deserialização do JSON retornado pela API, percorrendo o objeto e “montando” uma EntityCollection

CreateExternalActivity – recebe um objeto ExternalActivity e transforma em uma Entity, tenha cuidado com a conversão dos tipos de dados que o Dynamics está esperando recebemos, por exemplo, o atributo “new_contactid” é um lookup, assim temos que passar uma EntityReference.

Agora vamos aos plugins!

Retrieve.cs

using Microsoft.Xrm.Sdk;
using System;
using System.Threading.Tasks;

namespace MyCustomDataProvider
{
    public class Retrieve : IPlugin
    {
        #region Secure/Unsecure Configuration Setup
        private string _secureConfig = null;
        private string _unsecureConfig = null;

        public Retrieve(string unsecureConfig, string secureConfig)
        {
            _secureConfig = secureConfig;
            _unsecureConfig = unsecureConfig;
        }
        #endregion
        public void Execute(IServiceProvider serviceProvider)
        {
            ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = factory.CreateOrganizationService(context.UserId);

            try
            {
                EntityCollection results;
                EntityReference target = (EntityReference)context.InputParameters["Target"];

                if (target != null)
                {
                    string keyword = target.Id.ToString();

                    tracer.Trace($"Target: {keyword}");

                    string url = $"http://HOST/DemoJSONService/JSONService.svc/ExternalActivity/{keyword}";

                    tracer.Trace($"API URL : {url}");
                    tracer.Trace("Calling API");

                    // Call API through async method
                    var getAPIDataTask = Task.Run(async() = > await Helper.GetAPIData(tracer, url));
                    Task.WaitAll(getAPIDataTask);

                    tracer.Trace("API has been called");

                    // Parse API results to Entity Collection
                    results = ExternalActivity.FillEntityCollection(tracer, getAPIDataTask.Result);

                    tracer.Trace("Business Entity Collection has been filled up with API response");

                    if (results != null && results.Entities.Count > 0)
                    {
                        // Return Business Collection Object filled with API response
                        context.OutputParameters["BusinessEntity"] = results[0];
                    }
                }
            }
            catch (Exception e)
            {
                tracer.Trace($"Message: {e.Message}");
                tracer.Trace($"StackTrace: {e.StackTrace}");

                if (e.InnerException != null)
                {
                    tracer.Trace($"InnerException Message: {e.InnerException.Message}");
                    tracer.Trace($"InnerException StackTrace: {e.InnerException.StackTrace}");
                }

                throw new InvalidPluginExecutionException(e.Message);
            }
        }
    }
}

Linhas Destacadas:

33 – Recuperamos o ExternalActivityId do contexto(Target)

37 – URL da API Externa

43 e 44 – Invocamos o método assíncrono GetAPIData

49 – Resultado da API é transformado em uma EntityCollection

53 à 57 – OutputParameter do tipo BusinessEntity é populado com a primeira (e única linha) da EntityCollection

 

RetrieveMultiple.cs

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Extensions;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Threading.Tasks;

namespace MyCustomDataProvider
{
    public class RetrieveMultiple : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = factory.CreateOrganizationService(context.UserId);

            try
            {
                QueryExpression query = context.InputParameterOrDefault<QueryExpression>("Query");
                EntityCollection results;
                SearchVisitor visitor = new SearchVisitor();
                query.Accept(visitor);

                string keyword = visitor.SearchKeyWord;
                string url = $"http://HOST/DemoJSONService/JSONService.svc/ExternalActivities/{keyword}";

                tracer.Trace($"Keyword searched: {keyword}");
                tracer.Trace($"API URL : {url}");
                tracer.Trace("Calling API");

                // Call API through async method
                var getAPIDataTask = Task.Run(async() = > await Helper.GetAPIData(tracer, url));
                Task.WaitAll(getAPIDataTask);

                tracer.Trace("API has been called");

                // Parse API results to Entity Collection
                results = ExternalActivity.FillEntityCollection(tracer, getAPIDataTask.Result);

                tracer.Trace("Business Entity Collection has been filled up with API response");

                // Return Business Collection Object filled with API response
                context.OutputParameters["BusinessEntityCollection"] = results;
            }
            catch (Exception e)
            {
                tracer.Trace($"Message: {e.Message}");
                tracer.Trace($"StackTrace: {e.StackTrace}");

                if (e.InnerException != null)
                {
                    tracer.Trace($"InnerException Message: {e.InnerException.Message}");
                    tracer.Trace($"InnerException StackTrace: {e.InnerException.StackTrace}");
                }

                throw new InvalidPluginExecutionException(e.Message);
            }
        }
    }
}

Linhas Destacadas:

22-25 – Recuperamos o ContactId do contexto utilizando a classe SearchVisitor

26 – URL da API Externa

33 e 34 – Invocamos o método assíncrono GetAPIData

39 – Resultado da API é transformado em uma EntityCollection

44 – OutputParameter do tipo BusinessEntityCollection é populado

 

Chega de código, vamos registrar o provedor de dados! Este será meu próximo post, não percam!

Para maiores detalhes, seguem os documentos oficiais:

Custom virtual entity data providers

Sample: Generic virtual entity data provider plug-in

[]’s,

Tiago

2 comentários em “Dynamics 365 – Provedor de Dados Customizado/Custom Data Provider (Parte 2/3)

  1. Boa noite Parabéns pelo conteúdo.
    Deixa eu falar, tem algum material relacionado a consulta de tabelas via API json, preciso consumir as tabelas em uma plataforma de BI que não tem integração como o Power BI.

    Curtir

Deixe um comentário

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.