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

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.
CurtirCurtir
Ola Caio,
Obrigado! Toda a plataforma pode ser acessada atraves da url da sua organizacao + /api/9.1/ + nome da tabela no plural. Algo assim: https://SUAORG.crm.dynamics.com/api/data/v9.1/accounts. Voce pode navegar direto pelo browser onde vc ja esteja logado em sua organizacao.
[]’s,
Tiago
CurtirCurtir