D365: Azure Durable Function with D365 CE – Part 6

In the previous post, we saw how to invoke webhook using custom global action and passed plugin execution context to the webhook. In this last post of this series, we’ll see how to execute custom global action from JS on form load.

Using CRM Rest Builder, we can get the HttpRequest for executing action which is as below:

DXC.ExecuteAction = function (executionContext) {
    var formContext = executionContext.getFormContext();
    var globalContext = Xrm.Utility.getGlobalContext();

    var parameters = {};
    parameters.ServiceCeaseDate = new Date(2019, 5, 15);
    parameters.TargetEntity = formContext.data.entity.getEntityName();
    parameters.TargetID = formContext.data.entity.getId();

    var req = new XMLHttpRequest();
    req.open("POST", globalContext.getClientUrl() + "/api/data/v9.1/dxc_ACUpdateAccommodationPayments", true);
    req.setRequestHeader("OData-MaxVersion", "4.0");
    req.setRequestHeader("OData-Version", "4.0");
    req.setRequestHeader("Accept", "application/json");
    req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
    req.onreadystatechange = function () {
        if (this.readyState === 4) {
            req.onreadystatechange = null;
            if (this.status === 204) {
                //Success - No Return Data - Do Something
            } else {
                Xrm.Utility.alertDialog(this.statusText);
            }
        }
    };
    req.send(JSON.stringify(parameters));
};

We are passing the input parameters on Line 6-8 highlighted above. Once the action gets executed, webhook will be invoked and plugin context will be passed to it(as shown in the previous post) which in turn will be passed to the azure durable function.

So, we need to modify our durable function(shown in Part 3 of this series) a bit so that the plugin context that gets passed to the durable function in JSON format gets deserialized properly.

So, the code for HTTP triggered function is as shown below:

 [FunctionName("AccommodationPayments")]
        public static async Task<HttpResponseMessage> HttpStart(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequestMessage req,
            [OrchestrationClient] DurableOrchestrationClient starter,
            ILogger log)
        {
            var postedData = await req.Content.ReadAsStringAsync();
            log.LogInformation($"Posted Data = '{postedData}'.");

            if (string.IsNullOrWhiteSpace(postedData))
            {
                return req.CreateResponse(HttpStatusCode.BadRequest, new { summary = "Posted data is empty" });
            }

            string instanceId = await starter.StartNewAsync("AccommodationPayments_OrchestrationFunction", postedData);

            log.LogInformation($"Started orchestration with ID = '{instanceId}'.");

            return starter.CreateCheckStatusResponse(req, instanceId);
        }

The logging at Line 8(highlighted above) will be helpful to show the plugin context passed to the durable function in JSON format as shown below:

{
	"BusinessUnitId": "1b4721fb-7058-e911-a959-000d3a3adab0",
	"CorrelationId": "e1ba9f97-c971-4979-b373-12a6a24ce74e",
	"Depth": 1,
	"InitiatingUserId": "02ae254c-335b-e911-a84c-000d3a3627b3",
	"InputParameters": [
		{
			"key": "ServiceCeaseDate",
			"value": "/Date(1571922000000)/"
		},
		{
			"key": "TargetEntity",
			"value": "contact"
		},
		{
			"key": "TargetID",
			"value": "{32B54F5A-95A6-E911-A857-000D3A372186}"
		},
		{
			"key": "parentExecutionId",
			"value": "b16df3e1-f2d9-45e9-a4f3-0db9048e28ac"
		}
	],
	"IsExecutingOffline": false,
	"IsInTransaction": true,
	"IsOfflinePlayback": false,
	"IsolationMode": 2,
	"MessageName": "dxc_ACUpdateAccommodationPayments",
	"Mode": 0,
	"OperationCreatedOn": "/Date(1572306594631)/",
	"OperationId": "82f1d96c-b623-4da9-ae44-d4d5e27249dd",
	"OrganizationId": "efe26240-1da4-4e7c-9c4e-c384e4868abc",
	"OrganizationName": "orgdb6bc5d5",
	"OutputParameters": [],
	"OwningExtension": {
		"Id": "c5640f2e-e6f6-e911-a862-000d3a372932",
		"KeyAttributes": [],
		"LogicalName": "sdkmessageprocessingstep",
		"Name": "Dxc.AgedCare.Crm.CorePlugins.ExecuteActionUpdateAccommodationPayments: dxc_ACUpdateAccommodationPayments of any Entity",
		"RowVersion": null
	},
	"ParentContext": null,
	"PostEntityImages": [],
	"PreEntityImages": [],
	"PrimaryEntityId": "00000000-0000-0000-0000-000000000000",
	"PrimaryEntityName": "none",
	"RequestId": "82f1d96c-b623-4da9-ae44-d4d5e27249dd",
	"SecondaryEntityName": "none",
	"SharedVariables": [
		{
			"key": "IsAutoTransact",
			"value": true
		}
	],
	"Stage": 40,
	"UserId": "02ae254c-335b-e911-a84c-000d3a3627b3"
}

The highlighted lines above show the data that we passed to the durable function through webhook. For deserialzing it we would need appropriate object to hold this data. http://json2csharp.com/ is one of the link which creates C# type based on the JSON data passed to it.

Hence, the converted C# types for the above JSON data are as shown below:

  public class InputParameter
    {
        public string key { get; set; }
        public string value { get; set; }
    }

    public class OwningExtension
    {
        public string Id { get; set; }
        public List<object> KeyAttributes { get; set; }
        public string LogicalName { get; set; }
        public string Name { get; set; }
        public object RowVersion { get; set; }
    }

    public class SharedVariable
    {
        public string key { get; set; }
        public bool value { get; set; }
    }

    public class InputContext
    {
        public string BusinessUnitId { get; set; }
        public string CorrelationId { get; set; }
        public int Depth { get; set; }
        public string InitiatingUserId { get; set; }
        public List<InputParameter> InputParameters { get; set; }
        public bool IsExecutingOffline { get; set; }
        public bool IsInTransaction { get; set; }
        public bool IsOfflinePlayback { get; set; }
        public int IsolationMode { get; set; }
        public string MessageName { get; set; }
        public int Mode { get; set; }
        public DateTime OperationCreatedOn { get; set; }
        public DateTime ServiceCeaseDate { get; set; }
        public string OperationId { get; set; }
        public string OrganizationId { get; set; }
        public string OrganizationName { get; set; }
        public List<object> OutputParameters { get; set; }
        public OwningExtension OwningExtension { get; set; }
        public object ParentContext { get; set; }
        public List<object> PostEntityImages { get; set; }
        public List<object> PreEntityImages { get; set; }
        public string PrimaryEntityId { get; set; }
        public string PrimaryEntityName { get; set; }
        public string RequestId { get; set; }
        public string SecondaryEntityName { get; set; }
        public List<SharedVariable> SharedVariables { get; set; }
        public int Stage { get; set; }
        public string UserId { get; set; }
    }

Then the Orchestration function code is as shown below:

 [FunctionName("AccommodationPayments_OrchestrationFunction")]
        public static async Task<string> RunOrchestrator(
            [OrchestrationTrigger] DurableOrchestrationContext context, ILogger log)
        {
            var input = context.GetInput<string>();
            log.LogInformation($"Inside Orchestration");
            var data = JsonConvert.DeserializeObject<InputContext>(input);

            foreach (var parameter in data.InputParameters)
            {
                if (parameter.key.Equals("TargetEntity", StringComparison.InvariantCultureIgnoreCase))
                {
                    data.PrimaryEntityName = parameter.value;
                }

                if (parameter.key.Equals("TargetID", StringComparison.InvariantCultureIgnoreCase))
                {
                    data.PrimaryEntityId = parameter.value.Replace("{", "").Replace("}", "");
                }

                if (parameter.key.Equals("ServiceCeaseDate", StringComparison.InvariantCultureIgnoreCase))
                {
                    string datetime = parameter.value;
                    datetime = datetime.Replace(@"\", "").Replace("/", "").Replace("Date", "").Replace("(", "").Replace(")", "");
                    DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc);
                    dtDateTime = dtDateTime.AddMilliseconds(Convert.ToDouble(datetime)).ToUniversalTime().Date;
                    data.ServiceCeaseDate = dtDateTime;
                    log.LogInformation($"ServiceCeaseDate: {data.ServiceCeaseDate.ToString()}");
                }
            }

            log.LogInformation($"Target Entity: {data.PrimaryEntityName}. Target Record: {data.PrimaryEntityId}");

            return await context.CallActivityAsync<string>("AccommodationPayments_ActivityFunction", data);
        }

The highlighted line above is responsible for deserializing the plugin context passed to durable function to InputContext object.

The code for Activity function is as shown below:

  [FunctionName("AccommodationPayments_ActivityFunction")]
        public static async Task<string> RetrieveRecords([ActivityTrigger] InputContext input, ILogger log)
        {
            string resultMessage = "Request processed successfully.";
            var crmOrganizationUrl = Environment.GetEnvironmentVariable("CRMOrganization");
            var crmOrganizationVersionUrl = Environment.GetEnvironmentVariable("CRMOrganizationVersionUrl");
            var crmUrl = $"{crmOrganizationUrl}{crmOrganizationVersionUrl}";
            var resultAccessToken = await CRMWebApiHelper.GetAccessToken();
            log.LogInformation($"Access Token received");
            var crmApi = new CRMWebAPI(crmUrl, resultAccessToken);
            try
            {
                var recordId = input.PrimaryEntityId;

                var results = GetAccommodationPayments(crmApi, recordId).ToList();
                log.LogInformation($"Got accommodation payments");

                foreach (var entity in results)
                {
                    var accommodationPaymentDictionary = new Dictionary<string, object>();
                    accommodationPaymentDictionary.Add("dxc_expectedrefundduedate", input.ServiceCeaseDate.Date.ToString("yyyy-MM-dd"));
                    var response = crmApi.Update("dxc_accommodationpayments", entity.accommodationPaymentId, accommodationPaymentDictionary).Result;
                }
            }
            catch (Exception ex)
            {
                log.LogError($"Accommodation Payment Update Failed for Contact: {input.PrimaryEntityId} - StackTrace : {ex.StackTrace}  - Exception Message : {ex.Message}");
                resultMessage = ex.Message;
            }
            return resultMessage;
        }

The helper method to get related records of contact record is as shown below:

 private static IEnumerable<AccommodationPaymentObject> GetAccommodationPayments(CRMWebAPI api, string contactId)
        {
            var xml = @"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
                          <entity name='dxc_accommodationpayment'>
                            <attribute name='dxc_accommodationpaymentid' />
                            <filter type='and'>
                              <condition attribute='dxc_clientid' operator='eq' uitype='contact' value='{" + contactId + @"}' />
                              <condition attribute='dxc_actualrefunddate' operator='null' />
                            </filter>
                          </entity>
                        </fetch>";
            var options = new CRMGetListOptions()
            {
                FormattedValues = true,
                FetchXml = xml
            };
            var result = api.GetList("dxc_accommodationpayments", options);

            if (result == null || result.Result == null || result.Result.List.Count == 0)
                return new List<AccommodationPaymentObject>();

            return (from r in result.Result.List.Cast<IDictionary<string, object>>()
                    select new AccommodationPaymentObject
                    {
                        accommodationPaymentId = Guid.Parse(r["dxc_accommodationpaymentid"].ToString())
                    });
        }

Once done, let’s deploy the azure durable unction to Azure as shown in Part 3 of this series.

Before testing the entire functionality, let’s check the value of the records in the system which is as shown below:

After deploying the durable function to Azure, let’s test the code of calling custom global action on load of contact form(for demo purpose).
Once the global action is executed, the input parameters that are passed to it will be sent to webhook as part of plugin context when the webhook is invoked using custom global action.
Once the webhook is invoked, the plugin context will be passed to the azure durable function and will do further processing i.e. it will retrieve the related records of the contact record and will update their Expected Refund Due Date field.

Once the durable function is executed, we can check the status of the durable function and logging on Azure portal as shown in Part 3 of this series. After successful execution of durable function let’s see the value of the records back in D365 which is as shown below:

We can see that the date time value passed as input parameter has been reflected in Expected Refund Due Date.

So, in the final post of this series we saw how we can execute custom global action using JS based on any business requirement passing input parameters to it.

Hope it helps !!

Advertisements

2 thoughts on “D365: Azure Durable Function with D365 CE – Part 6

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.