首页 > 解决方案 > 在 Dynamics CRM 插件中:如何强制代码不等待异步响应

问题描述

所以这是我的场景。我必须通过 Dynamics CRM 插件代码 (C#) 异步调用 Azure 函数,这很好。但我不希望代码等待 Azure Function 的响应。我只想完成代码执行并退出。

如有必要,Azure 函数将处理 CRM 中的更新。

我不想等待的原因是在 CRM Online 中完成插件执行有 2 分钟的时间限制。但是,Azure Function 可能需要几分钟才能完成该过程。

这是我对 Azure Function 进行同步调用的插件类代码。(我可以在本文档之后将调用转换为异步,但按照这种方法,我的代码仍将等待响应)。

public class CallAzureFunc : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        // Extract the tracing service for use in debugging sandboxed plug-ins.  
        ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

        // Obtain the execution context from the service provider.  
        IPluginExecutionContext context = (IPluginExecutionContext) serviceProvider.GetService(typeof(IPluginExecutionContext));

        // The InputParameters collection contains all the data passed in the message request.  
        if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
        {
            // Obtain the target entity from the input parameters.  
            Entity entity = (Entity)context.InputParameters["Target"];

            // Verify that the target entity represents an entity type you are expecting.   
            if (entity.LogicalName != "account")
                return;

            // Obtain the organization service reference which you will need web service calls.  
            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

            try
            {
                // Plug-in business logic goes here.  

                Data data = new Data
                {
                    name = entity.Attributes["name"].ToString()
                };

                string result = CallFunction(tracer, data);
                tracer.Trace($@"result: {result}");
            }

            catch (FaultException<OrganizationServiceFault> ex)
            {
                throw new InvalidPluginExecutionException("An error occurred in MyPlug-in.", ex);
            }

            catch (Exception ex)
            {
                tracer.Trace("MyPlugin: {0}", ex.ToString());
                throw;
            }
        }
    }

    private string CallFunction(ITracingService tracer, Data data)
    {
        string json = JsonConvert.SerializeObject(data);

        string apiUrl = "https://<AzureFunctionName>.azurewebsites.net/api/";
        string token = "<token>";
        string content = null;
        string apiMethod = "CreateContactFromLead";
        string inputJson = json;
        string result = ApiHelper.ExecuteApiRequest(apiUrl, token, content, apiMethod, inputJson, tracer);
        return result;
    }
}

以下是进行 API 调用的辅助方法。

    internal static string ExecuteApiRequest(string apiUrl, string token, string content, string apiMethod, string inputJson, ITracingService tracer)
    {
        try
        {
            var data = Encoding.ASCII.GetBytes(inputJson);

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(String.Format(apiUrl + apiMethod));
            request.Method = "POST";
            request.ContentLength = inputJson.Length;
            request.ContentType = "application/json";
            request.ContentLength = data.Length;
            request.Headers.Add("x-functions-key", token);
            request.Accept = "application/json";

            // Send the data
            Stream newStream = request.GetRequestStream();
            newStream.Write(data, 0, data.Length);
            newStream.Close();

            // Get the resposne
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();

            if (response != null)
            {
                tracer.Trace("ApiHelper > ExecuteApiRequest > response.StatusCode: " + response.StatusCode);
                tracer.Trace("ApiHelper > ExecuteApiRequest > response.StatusDescription: " + response.StatusDescription);
            }

            if (response.StatusCode == HttpStatusCode.OK || response.StatusDescription == "OK" || response.StatusDescription == "200")
            {
                content = ReadStream(response, tracer);
            }
            else if (response.StatusCode == HttpStatusCode.NoContent || response.StatusDescription == "No Content" || response.StatusDescription == "204")
            {
                content = null;
            }
            else
            {
                if (response != null)
                {
                    throw new Exception(String.Format("Status Code: {0}, Status Description: {1}",
                        response.StatusCode,
                        response.StatusDescription));
                }
            }

            return content;
        }
        catch (Exception ex)
        {
            tracer.Trace("ApiHelper > ExecuteApiRequest > error: " + ex.Message);
            throw;
        }
    }

    private static string ReadStream(HttpWebResponse response, ITracingService tracer)
    {
        try
        {
            var responseJson = string.Empty;
            if (response != null)
            {
                Stream dataStream = response.GetResponseStream();
                if (dataStream != null)
                {
                    using (StreamReader reader = new StreamReader(dataStream))
                    {
                        while (!reader.EndOfStream)
                        {
                            responseJson = reader.ReadToEnd();
                        }
                    }
                }
            }
            return responseJson;
        }
        catch (Exception ex)
        {
            tracer.Trace("ApiHelper > ReadStream > error: " + ex.Message);
            throw ex;
        }
    }

标签: c#asynchronousasync-awaitdynamics-crmazure-functions

解决方案


你需要两个功能。

函数 #1 将由您的插件调用(本质上是您现在正在执行的操作。)它将验证输入。如果输入成功,它将在Azure Service Bus Queue中放置一条消息(可能包括来自调用者的相关数据) 。将消息放入服务总线队列后,它将终止并向调用者返回成功消息(即插件代码。)

函数 #2 将由 Azure 服务总线队列消息触发。此函数将根据消息内容处理长时间运行的代码(来自函数 #1。)

Azure 服务触发 Azure 功能的示例

[FunctionName("ServiceBusQueueTriggerCSharp")]                    
public static void Run(
    [ServiceBusTrigger("myqueue", AccessRights.Manage, Connection = "ServiceBusConnection")] 
    string myQueueItem,
    Int32 deliveryCount,
    DateTime enqueuedTimeUtc,
    string messageId,
    TraceWriter log)
{
    log.Info($"C# ServiceBus queue trigger function processed message: {myQueueItem}");
    log.Info($"EnqueuedTimeUtc={enqueuedTimeUtc}");
    log.Info($"DeliveryCount={deliveryCount}");
    log.Info($"MessageId={messageId}");
}

这种模式很常用,因为它提供了事务执行的安全性。如果您只有一个函数,如上所述,并且函数失败,则调用将丢失,因为没有用于完成的侦听器。

使用两个函数我们有安全性。如果 Function #1 失败(验证或将消息放入队列)它将失败给调用者,并且您的插件代码可以适当地处理。如果功能 #2 失败,它将故障返回到服务总线并排队等待重试(默认情况下,它最多重试 5 次,然后写入毒队列。)


推荐阅读