Olá pessoal,
Neste post, irei demonstrar como criar um custom workflow assembly para realizar chamadas HTTP, desta forma podemos acionar Azure Functions por uma Model-driven App/Dynamics 365 CE.
Portanto, para este post será necessário que você possua uma conta no Azure, além é claro de um ambiente Dynamics 365 CE/Model-driven App.
Neste exemplo irei criar um workflow sob demanda para chamar o custom workflow que posteriormente acionará a Azure function. Assim, podemos dividir o post em três etapas: Azure Function, Custom Workflow e Workflow. Obviamente focarei mais no Custom Workflow, visto que já temos vários exemplos por ai de como criar Azure Functions e o workflow será algo extremamente simples, apenas para chamar o custom workflow.
Azure Function
Crie uma Azure function utilizando modelo pré configurado de uma HTTP Trigger call:
#r "Newtonsoft.Json"
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
string responseMessage = string.IsNullOrEmpty(name)
? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
: $"Hello, {name}. This HTTP triggered function executed successfully.";
return new OkObjectResult(responseMessage);
}
Como falei anteriormente não entrarei nos detalhes de como fazer e configurar sua Azure function, mas a dica aqui é de ter certeza de que ela está funcionando se apenas chamá-la utilizando um Postman da vida:
Custom Workflow
Agora vamos ao que interessa neste post! Criar um custom workflow para que seja possível chamar uma Azure function por ele. Na verdade, o custom workflow realizará uma chamada HTTP, com isso não estamos restritos apenas a uma Azure function, podemos chamar qualquer API por ele.
Vamos ao código, depois explicarei sobre ele:
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Workflow;
using System;
using System.Activities;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace TMC.Workflows
{
public class CallAzureFunction : BaseClass
{
[Input("Request Headers")]
public InArgument<string> RequestHeaders { get; set; }
[Input("Request Body")]
public InArgument<string> RequestBody { get; set; }
[RequiredArgument]
[Input("Request Url")]
public InArgument<string> RequestUrl { get; set; }
[RequiredArgument]
[Input("Request Method (GET, POST, PUT, DELETE, PATCH)")]
public InArgument<string> RequestMethod { get; set; }
[Output("Response Headers")]
public OutArgument<string> ResponseHeaders { get; set; }
[Output("Response Body")]
public OutArgument<string> ResponseBody { get; set; }
public override void ExecuteWorkflow()
{
tracingService.Trace($"Starting custom workflow {className}");
string body = string.Empty;
var content = new StringContent(body, Encoding.UTF8, "application/json");
Task<HttpResponseMessage> response;
RequestMethodEnum requestMethod;
StringBuilder delimitedHeaders = new StringBuilder();
try
{
// Check if there is a body to be sent
if (!string.IsNullOrEmpty(RequestBody.Get<string>(context)))
{
body = RequestBody.Get<string>(context);
}
tracingService.Trace($"body {body}");
using (var client = new HttpClient())
{
requestMethod = GetRequestMethod(RequestMethod.Get<string>(context));
tracingService.Trace($"requestMethod {requestMethod}");
tracingService.Trace($"Calling SendRequest...");
response = SendRequest(context, client, requestMethod, content);
tracingService.Trace($"SendRequest was called");
// Check if reponse was a success, otherwise thrown an exception
response.Result.EnsureSuccessStatusCode();
tracingService.Trace($"HTTP call was called successfully");
// Add a delimiter (;) for every header row returned
foreach (var header in response.Result.Headers)
{
if (delimitedHeaders.Length > 0)
{
delimitedHeaders.Append(";");
}
delimitedHeaders.Append($"{header.Key}:{header.Value}");
}
// Set Response Header output parameter
ResponseHeaders.Set(context, delimitedHeaders.ToString());
tracingService.Trace($"ResponseHeaders {ResponseHeaders.Get<string>(context)}");
// Set Response Body output parameter
var responseString = response.Result.Content.ReadAsStringAsync();
ResponseBody.Set(context, responseString.Result);
tracingService.Trace($"ResponseBody {ResponseBody.Get<string>(context)}");
}
}
catch (Exception ex)
{
tracingService.Trace($"Exception: {ex.Message}");
tracingService.Trace($"StackTrace: {ex.StackTrace}");
tracingService.Trace($"InnerException: {ex.InnerException}");
// Set Response Header output parameter
ResponseHeaders.Set(context, "HTTP call was failed");
// Instead of throwing an error, sending back the response header with a texting indicating an error
//throw new InvalidPluginExecutionException(ex.Message);
}
finally
{
tracingService.Trace($"ResponseHeaders {ResponseHeaders.Get<string>(context)}");
tracingService.Trace($"ResponseBody {ResponseBody.Get<string>(context)}");
tracingService.Trace($"Finished custom workflow {className}");
}
}
private async Task<HttpResponseMessage> SendRequest(CodeActivityContext context, HttpClient client, RequestMethodEnum requestMethod, StringContent content)
{
switch (requestMethod)
{
case RequestMethodEnum.GET:
return await client.GetAsync(RequestUrl.Get<string>(context));
case RequestMethodEnum.PATCH:
return await client.PatchAsync(RequestUrl.Get<string>(context), content);
case RequestMethodEnum.PUT:
return await client.PutAsync(RequestUrl.Get<string>(context), content);
case RequestMethodEnum.DELETE:
return await client.DeleteAsync(RequestUrl.Get<string>(context));
case RequestMethodEnum.POST:
return await client.PostAsync(RequestUrl.Get<string>(context), content);
default:
throw new InvalidPluginExecutionException("The Request Method supplied is not supported. Try using GET, PATCH, PUT, DELETE or POST");
}
}
private RequestMethodEnum GetRequestMethod(string requestMethod)
{
switch (requestMethod)
{
case "GET":
return RequestMethodEnum.GET;
case "POST":
return RequestMethodEnum.POST;
case "PUT":
return RequestMethodEnum.PUT;
case "DELETE":
return RequestMethodEnum.DELETE;
case "PATCH":
return RequestMethodEnum.PATCH;
default:
return RequestMethodEnum.DEFAULT;
}
}
private enum RequestMethodEnum
{
GET = 0,
POST = 1,
PUT = 2,
DELETE = 3,
PATCH = 4,
DEFAULT = 999
}
}
public static class HttpClientExtensions
{
public static async Task<HttpResponseMessage> PatchAsync(this HttpClient client, string requestUrl, HttpContent iContent)
{
Uri requestUri = new Uri(requestUrl);
var method = new HttpMethod("PATCH");
var request = new HttpRequestMessage(method, requestUri)
{
Content = iContent
};
HttpResponseMessage response = new HttpResponseMessage();
return await client.SendAsync(request);
}
}
}
Bem no início do meu código, podemos ver 4 parametros de entrada (input) e 2 saídas (output).
Para entrada temos:
- RequestHeaders: caso a API que deseja acessar necessite algum header;
- RequestBody: sem estamos fazendo um POST por exemplo, geralmente estamos requisitando algo, assim o body da call terá alguma coisa.
- RequestUrl (obrigatório): URL da API que desejamos acionar
- RequestMethod (obrigatório): qual o método para a chamada API será usado (GET, POST, PATCH, PUT, DELETE)
Para saída temos:
- ResponseHeaders: header recebido após a chamada da API;
- ResponseBody: body retornado pela API;
O restante do código basicamente verifica qual é o método para realizar a chamada HTTP através do objeto HTTPClient. Posteriormente a chamada é realizada, havendo algum erro, não estou lançando uma exceção, ao invés disso estou retornando a informação de que algo inesperado ocorreu. Caso tudo dê certo, header e body serão populados com as informações recebidas da API.
No exemplo acima fiz uso de uma classe Base para acelerar o desenvolvimento do custom workflow, o código está aqui:
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Workflow;
using System;
using System.Activities;
namespace TMC.Workflows
{
public abstract class BaseClass : CodeActivity
{
public IOrganizationService service { get; private set; }
public CodeActivityContext context { get; private set; }
public IWorkflowContext workflowContext { get; private set; }
public IOrganizationServiceFactory serviceFactory { get; private set; }
public ITracingService tracingService { get; private set; }
public string className { get; private set; }
[MTAThread]
protected override void Execute(CodeActivityContext codeActivityContext)
{
className = GetType().Name;
context = codeActivityContext;
workflowContext = context.GetExtension<IWorkflowContext>();
serviceFactory = context.GetExtension<IOrganizationServiceFactory>();
tracingService = context.GetExtension<ITracingService>();
service = serviceFactory.CreateOrganizationService(workflowContext.UserId);
this.ExecuteWorkflow();
}
public abstract void ExecuteWorkflow();
}
}
Registre o workflow para ele ficar disponível no Dynamics 365 CE/Model-driven App.
Workflow
Por fim, precisamos de uma forma de chamar o custom workflow. A forma que escolhi foi de criar um workflow síncrono e que pode ser executado sob demanda (apenas para carater de teste):
Configure os parametros de entrada:
Para fins ilustrativos, estou criando uma tarefa (task) com os valores retornados pela Azure Function:
Pronto tudo preparado, vamos ao teste!
Acione o workflow manualmente:
Agora vamos checar a tarefa recem criada:
Para baixar o código completo, entre no meu GitHub: https://github.com/TiagoMCardoso/CallAzureFunction
[]’s,
Tiago