Nas tecnologias atuais, o uso de processamento assíncrono é algo muito comum no dia-a-dia. Porém até o momento o Dataverse não oferecia nenhum recurso onde poderiamos requisitar trabalho em segundo plano e acompanhar o processo. Atualmente temos plugins e workflows que rodam de maneira assíncrona, porén não nos permite acompanhar o processo, somente identificamos quando quando o registro é alterado ou criado, necessitando ficar criando controles para validação da execução.
Mas a partir de agora isso esta prestes a mudar. Em seu release wave 2 de 2023, uma nova funcionalidade já está habilitada em modo preview onde poderemos solicitar execução de trabalho em segundo plano e com isso acompanhar o progresso. Neste artigo, vou apresentar como utilizar essa nova funcionalidade e algumas dicas de como tirar melhor proveito dela.
Processos em Segundo Plano
O processos em segundo plano são requisições que fazemos ao Dataverse ele processa de maneira assincrona, permitindo que você não fique preso na plataforma para avaliar o resultado.
Para conseguir utilizar os processos em segundo plano você deverá criar uma Api Customizada (Custom API). Por questões de evolução da plataforma ela não permite fazer com workflows ou custom actions. Em um futuro próximo as custom actions serão substituídas por Api Customizadas. Aqui vale um ponto de atenção, as APIs Customizadas utilizam plugin para execução com isso, sabemos que o tempo de execução é de no máximo 2 minutos, valendo também para as operações em segundo plano.
Para poder solicitar um processo, o usuário devera ter acesso de leitura e escrita na tabela backgroundoperation . Se você tem o direito de acesso de Customizador do Sistema ou Administrador do sistema, não precisa se preocupar com isso.
A solicitação da execução pode ser feita tando pelo SDK ou pela WebApi. Nesse artigo irei apresentar através do SDK.
Ao solicitar a execução podemos validar o seu status de duas formas: Através da tabela background operations ou pelo monitor de recuros.
Tabela background operations
Utilizando a tabela backgroundoperations devemos executar uma queryexpression ou um fetchxml para obter as informações. Porém devemos sempre obter os seguintes campos:
- Name : o nome do processo em segundo plano
- Backgroundoperationstatecode: O status do processo
- Backgroundoperationsstatuscode: A razão do status
- Outputparameters: os parametros de saida.
- Aqui os valores são representados em um JSON de chave e valor
- Errorcode: Código em caso de erro
- Errormessage: a mensagem em caso de erro.
<fetch>
<entity name="backgroundoperation">
<attribute name="backgroundoperationstatecode" />
<attribute name="backgroundoperationstatuscode" />
<attribute name="errorcode" />
<attribute name="errormessage" />
<attribute name="name" />
<attribute name="outputparameters" />
</entity>
</fetch>
Monitor de Recursos
Utilizando um método GET apontando para api de processo em segundo plano (/api/backgroundoperations), podemos passar o id do processo que ele irá as mesma informações que foram solicitada na tabela backgroundoperations. Porém os parâmetros e informações de erro somente retornam quando o parâmetro for adicionado na API Customizada ou quando houver erro na execução.
Além disso, podemos ser notificados quando o processo finalizar, e é nesse ponto que eu vejo muita vantagem no uso. Pois podemos notificar de duas maneiras: Com um callback para uma URL ou registrando no evento “OnBackgroundOperationComplete”.
Para isso basta adicionar no request da requisição o parametro “CAlllbackUri” com a url.
E quando finalizado será feito um post para url informada com a seguinte estrutura:
{
"location": "<Url do processo em segundo plano>",
"backgroundOperationId": "{GUID}",
"backgroundOperationStateCode": {INT},
"backgroundOperationStatusCode": {INT},
"backgroundOperationErrorCode": {INT},
"backgroundOperationErrorMessage": {string},
}
Evento “OnBrackgroundOperationComplete”
Nesse caso o processo é mais simples, podemos registrar um plugin nesse evento permitindo atua quando a operação for finalizada. Além de plugin pode ser uma mensagem para service bus ou event hub também.
Onde irão vir os mesmo parâmetros citados acima no contexto da execução.
Exemplo de Uso
Vamos supor que ao concluir uma Pedido o seu cliente deseja que faça o envio da nota fiscal, e quando concluído você precisa informar para uma API que esta nota foi enviada e seguir o processo da venda.
Para isso criei uma Custom API que simula o envio da nota fiscal.
Com a custom API criada, agora vou criar um console que irá disparar uma processo em segundo plano que irá chamar uma API após a conclusão.
Vamos criar um método que solicita a execução da custom API como um processo em segundo plano:
public static Guid ExecutarTrabalhoSegundoPlano(CrmServiceClient client, bool withCallback = false)
{
//Cria o request da action
var request = new OrganizationRequest("mfp_enviar_nota_fiscal")
{
Parameters = {
{
"Target", new EntityReference("salesorder", new Guid("fcaca626-c39e-ee11-be37-0022482a3612"))
}
}
};
//Cria Request do trabalho em segundo plano
var requestBackground = new OrganizationRequest("ExecuteBackgroundOperation")
{
Parameters = {
{ "Request",request},
{
"CallbackUri", "https://typedwebhook.tools/webhook/d49eb4d9-681a-4534-b82a-7106e3433ce3"
}
}
};
//Executa o comando
var response = client.Execute(requestBackground);
var backgroundId = new Guid(response["BackgroundOperationId"].ToString());
Console.WriteLine($"BackgroundOperationId: {backgroundId}");
return backgroundId;
}
Além disso vamos criar um método para consultar o status, passamos o id do processo obtido no primeiro método para obter as informações:
public static BackgroundStatus ConsultarStatusProcesso(CrmServiceClient client, Guid backgroundId)
{
var columnSet = new ColumnSet(
"name",
"backgroundoperationstatecode",
"backgroundoperationstatuscode",
"outputparameters",
"errorcode",
"errormessage");
try
{
// Get the entity with all the required columns
var backgroundOperation = client.Retrieve("backgroundoperation", backgroundId, columnSet);
Console.WriteLine($"Name: {backgroundOperation["name"]}");
Console.WriteLine($"State Code: {backgroundOperation.FormattedValues["backgroundoperationstatecode"]}");
Console.WriteLine($"Status Code: {backgroundOperation.FormattedValues["backgroundoperationstatuscode"]}");
Console.WriteLine(Environment.NewLine);
return (BackgroundStatus)backgroundOperation.GetAttributeValue<OptionSetValue>("backgroundoperationstatuscode").Value;
}
// Catch Dataverse errors
catch (FaultException<OrganizationServiceFault> ex)
{
Console.WriteLine($"ErrorCode:{ex.Detail.ErrorCode}");
Console.WriteLine($"Message:{ex.Detail.Message}");
return BackgroundStatus.Failed;
}
// Catch other errors
catch (Exception error)
{
Console.WriteLine($"Some other error occurred: '{error.Message}'");
return BackgroundStatus.Failed;
}
}
E também vamos criar outros dois métodos: Uma para obter o retorno do execução e o outro para obter o erro em caso de falha.
public static List<KeyValuePair<string, string>> ObterRetornoProcesso(CrmServiceClient client, Guid backgroundId)
{
// List of columns that will help to get status, output and error details if any
var columnSet = new ColumnSet(
"name",
"outputparameters",
"errorcode"
);
try
{
// Get the entity with all the required columns
var backgroundOperation = client.Retrieve("backgroundoperation", backgroundId, columnSet);
Console.WriteLine($"Output Parameters:");
// Deserialize the Output Parameters into KeyValuePair<string, string>
List<KeyValuePair<string, string>> output =
System.Text.Json.JsonSerializer
.Deserialize<List<KeyValuePair<string, string>>>((string)backgroundOperation["outputparameters"]);
return output;
}
// Catch Dataverse errors
catch (FaultException<OrganizationServiceFault> ex)
{
Console.WriteLine($"ErrorCode:{ex.Detail.ErrorCode}");
Console.WriteLine($"Message:{ex.Detail.Message}");
}
// Catch other errors
catch (Exception error)
{
Console.WriteLine($"Some other error occurred: '{error.Message}'");
}
return null;
}
public static void ConsultarErro(CrmServiceClient client, Guid backgroundId)
{
var columnSet = new ColumnSet(
"name",
"errorcode",
"errormessage");
try
{
// Get the entity with all the required columns
var backgroundOperation = client.Retrieve("backgroundoperation", backgroundId, columnSet);
Console.WriteLine($"Error Code: {backgroundOperation.GetAttributeValue<string>("errorcode")}");
Console.WriteLine($"Error Message: {backgroundOperation.GetAttributeValue<string>("errormessage")}");
}
// Catch Dataverse errors
catch (FaultException<OrganizationServiceFault> ex)
{
Console.WriteLine($"ErrorCode:{ex.Detail.ErrorCode}");
Console.WriteLine($"Message:{ex.Detail.Message}");
}
// Catch other errors
catch (Exception error)
{
Console.WriteLine($"Some other error occurred: '{error.Message}'");
}
}
Agora quando executamos nosso código, poderemos ver que durante um período o processo em segundo plano vai ficar com o status “In Progress” significa que está em execução e depois quando concluído mostra o retorno da custom API.
Além disso podemos ver que o callback url a chamada foi realizada informando que foi concluída, ou seja ao concluir notificou a API para continuidade do processo.
Como podemos ver o seu uso pode ser bem diverso e assim podemos fazer diversas ações e tomada de decisões sem precisar ficar fazendo consultas de registros ou montando algum processo para monitoramento. Agora o processo em segundo plano consegue suprir essa lacuna que tínhamos na plataforma.