Model-driven App – Acionar Azure Function


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

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s

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