Dynamics 365的Custom API介绍

我是微软Dynamics 365 & Power Platform方面的工程师/顾问罗勇,也是2015年7月到2018年6月连续三年Dynamics CRM/Business Solutions方面的微软最有价值专家(Microsoft MVP),欢迎关注我的微信公众号 MSFTDynamics365erLuoYong ,回复447或者20210626可方便获取本文,同时可以在第一间得到我发布的最新博文信息,follow me!

以前我们定义消息的方法就是使用操作(Action),操作可以是无代码的(使用Workflow设计器配置)也可以是使用代码,比如在Workflow设计器中调用自定义工作流活动,或者在这个操作(Action)的Post Operation阶段注册插件步骤。这些操作没有权限限制,也可以比较方便的通过Web API或者SOAP终结点(组织服务)或者Workflow/Cloud flow调用。我以前也不少文章介绍,这里列出来供大家参考:

现在微软扩展了操作,提供了一种新的Feature叫做Custom API,比较早的介绍文章参考 Introducing Custom API – The New Way of Creating Custom Actions in Dataverse .

目前的官方文档包括但不限于如下:

估计你首先关心的是,Custom API和操作(Action)有啥不同,我在项目实践的时候才好选择,官方Compare Custom Process Action and Custom API列了一个比较如下,我就不一一翻译了,我摘要点说一下:Custom API是代码优先的,一定要写代码,不能像操作那样可以无代码,Custom API可以做权限控制,限制用户能否调用(通过要求用户具有某种权限才能调用),而操作则不行。Custom API支持本地化的名称和描述,而操作则不行。Custom API可以是Function或者Action,而操作只能是Action。Custom API可以通过修改Solution中文件而直接编辑,但是对于Action来讲则是不受支持的开发方法。Custom API可以绑定至Table Collection,而操作则不行。Custom API的执行时间受2分钟限制,而操作则可以更长,当然操作中的单个插件/自定义工作流活动也受两分钟的限制。

Capability Custom Process Action Custom API Description
Declarative logic with workflow Yes No Workflow Actions can have logic defined without writing code using the Classic Workflow designer.
Custom APIs require a plug-in written in .NET to implement logic that is applied on the server.
Require specific privilege No Yes With Custom API you can designate that a user must have a specific privilege to call the message. If the user doesn’t have that privilege through their security roles or team membership, an error will be returned.
Define main operation logic with code Yes Yes With Custom Process Actions the main operation processes the Workflow definition which may include custom workflow activities. The code in these custom workflow activities is processed in the main operation together with any other logic in the workflow.

With Custom API the message creator simply associates their plug-in type with the Custom API to provide the main operation logic.
Block Extension by other plug-ins Yes Yes With Custom Process actions set the IsCustomProcessingStepAllowedForOtherPublishers managed property to true if you wish to allow 3rd party plug-ins to run when registered on the message for your custom process action. When set to false, only plug-ins from the same solution publisher will run when a plug-in step is registered for the message.

For Custom API, set the AllowedCustomProcessingStepType to control whether any plug-ins steps may be registered, or if only asynchronous plug-ins may be registered.
Make message private No Yes When you create a message using a Custom Process Action, it is exposed publicly in the endpoint for anyone else to discover and use. If someone else takes a dependency on the message you created, their code will be broken if you remove, rename, or change the input or output parameter signature in the future.

If you do not intend for your message to be used by anyone else, you can mark it as a private message. This will indicate that you do not support others using the message you create, and it will not be included in definitions of available functions or actions exposed by the Web API $metadata service definition. Classes for calling these messages will not be generated using code generation tools, but you will still be able to use it.
Localizable names and descriptions No Yes While Custom Process Actions provide for a friendly name for the custom action and any input and output parameters it uses, these values are not localizable. With Custom API you can provide localizable names and descriptions. These localized strings can then be bound to controls that provide a UI to use the message.
Create OData Function No Yes The Dataverse Web API is an OData web service. OData provides for two types of operations: Actions & Functions.
  • An Action is an operation that makes changes to data in the system. It is invoked using the Http POST method and parameters are passed in the body of the request.
  • Function is an operation that makes no change to data, for example an operation that simply retrieves data. It is invoked using an Http GET method and the parameters are passed in the URL of the request

Custom Process Actions are always Actions. Custom API provides the option to define custom Functions.
There is nothing to prevent you from defining all operations as Actions if you wish. But some operations may be best expressed using a GET request available by defining a function.
Note: The Power Automate Common Data Service (current environment) connector only exposes Actions currently.
Create a global operation not bound to a table Yes Yes Both provide the ability to define a global message not bound to a table.
Bind an operation to a table Yes Yes Both provide the ability to pass a reference to a specific table record by binding it to a table.
Bind an operation to a table collection No Yes Binding an operation to a table collection allows for another way to define the signature for the Custom API. While this does not pass a collection of tables as an input parameter, is restricts the context of the operation to that type of table collection. Use this when your operation works with a collection of a specific type of tables or your operation will return a collection of that type.
Compose or modify a custom API by editing a solution No Yes ISVs who build and maintain products that work with the Power Platform apply ALM practices that involve solutions. The data within a solution is commonly checked into a source code repository and checked out by a developer applying changes.

A Custom Process Action is defined by a XAML Windows Workflow Foundation document which is transported as part of a solution. However, creating new or editing existing workflow definitions outside of the workflow designer is not supported.

Custom API definitions are solution aware components included in a solution through a set of folders and XML documents. These files and the file structure enable transport the API from one environment to another. Because these are plain text files, changes can be made to them, or new APIs can be defined by working with these files. This method of defining Custom APIs is supported. More information: Create a Custom API with solution files.
Subject to 2 minute time limit No Yes A plug-in that implements the main operation for a Custom API is subject to the 2 minute time limit to complete execution.

A Custom Process Action is not technically limited to two minutes. If a step in the Workflow logic contains a custom workflow activity, that part will be limited to two minutes. But the entire workflow cannot run indefinitely. There are other limitations that will cause long-running Custom Process Actions to fail. More information: Watch out for long running actions

 

不讲那么多了,我们来做个简单的例子,一个动手的例子胜过千言万语。需要通过 https://make.powerapps.com 来创建Custom API.我这里先做个Function类型的Custom API,接收一个文本参数城市名称,返回这个城市的所有客户。

打开某个解决方案,点击 New > Custom API,如下图。

Dynamics 365的Custom API介绍

 

我这里设置如下,这些字段的含义请参考官方文档CustomAPI Table Columns ,我就不一一翻译了,值得一提的是Allowed Custom Processing Step Type、Binding Type、Bound Entity Logical Name、Is Function、Unique Name 这些列(以前叫字段)的值保存后不能更改,且设置且珍惜吧,也不用特别记忆,你会发现保存后这些字段变成只读了。我这里将Is Funciton设置为True,因为我这个只是涉及到获取数据,不涉及到更新数据,用Function调用起来等都更加方便,我这里将Is Private设置为No,这样元数据中可以看到。Unique Name要用上开发者前缀,比如我这里是ly,不然不让保存。Bound Type我设置围为Global,这样灵活些。Plugin Type这个列的值我没有设置,因为我还没有开发,晚点设置。

Dynamics 365的Custom API介绍

 

保存后继续通过解决方案中的 New > Custom API Request Parameter 来添加输入参数,我设置的如下,Custom API当然要选,我选前面创建的Custom API,Unique Name当然也要带前缀,Type可以看到有很多种,我这里用常用的String,我将Is Optional设置为No,因为我要求必须输入,具体的每个列的含义请参考 CustomAPIRequestParameter Table Columns 。

Dynamics 365的Custom API介绍

 

保存后继续通过解决方案中的 New > Custom API Response Property 来添加输出参数,我设置的如下,Custom API当然要选,我选前面创建的Custom API,Unique Name当然也要带前缀,Type可以看到有很多种,我这里用常用的String,具体的每个列的含义请参考CustomAPIResponseProperty Table Columns 。

Dynamics 365的Custom API介绍

 

然后就是写代码了,类似插件,基本知识可以参考我前面的博文 Dynamics 365中开发和注册插件介绍 ,我这里不罗嗦了直接上代码如下:

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.ServiceModel;
using System.Text;

namespace D365.Plugins
{
    public class CustomAPIGetAccountsByCityName : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            //获取日志服务
            ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            //写一些日志,方便跟踪
            tracingService.Trace($"Enter CustomAPIGetAccountsByCityName on {DateTime.UtcNow.ToString()}");
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService adminOrgSvc = serviceFactory.CreateOrganizationService(null);
            var returnVal = string.Empty;
            if (context.InputParameters.Contains("ly_CityName"))
            {
                //我这里是演示性质代码,认为返回的记录最多只有5000行,但是实际项目中要考虑,如果可能需要分页查询
                string fetchXml = string.Format(@"<fetch version='1.0' mapping='logical' distinct='false' no-lock='true'>
  <entity name='account'>
    <attribute name='name' />
    <attribute name='telephone1' />
    <filter type='and'>
      <condition attribute='address1_city' operator='eq' value='{0}' />
    </filter>
  </entity>
</fetch>", EncodeforXml(context.InputParameters["ly_CityName"].ToString()));
                var accountEC = adminOrgSvc.RetrieveMultiple(new FetchExpression(fetchXml));
                if (accountEC.Entities.Any())
                {
                    var lsResults = new List<Account>();
                    foreach(var entity in accountEC.Entities)
                    {
                        lsResults.Add(new Account()
                        {
                            Name = entity.GetAttributeValue<string>("name"),
                            MainPhone = entity.GetAttributeValue<string>("telephone1")
                        });
                    }
                    returnVal = GetJsonString(lsResults, typeof(List<Account>));
                }
            }
            context.OutputParameters["ly_Accounts"] = returnVal;
        }

        public string GetJsonString(object value, Type type)
        {
            string result = string.Empty;
            using (MemoryStream stream = new MemoryStream())
            {
                DataContractJsonSerializerSettings serializerSettings = new DataContractJsonSerializerSettings
                {
                    UseSimpleDictionaryFormat = true
                };

                DataContractJsonSerializer ser = new DataContractJsonSerializer(type, serializerSettings);
                ser.WriteObject(stream, value);
                stream.Position = 0;
                using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
                {
                    result = reader.ReadToEnd();
                }
            }

            return result;
        }

        public string EncodeforXml(string content)
        {
            if (!string.IsNullOrEmpty(content))
            {
                return content.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("'", "&apos;").Replace("\"", "&quot;");
            }
            else
            {
                return string.Empty;
            }
        }
    }

    public class Account {
        public string Name { get; set; }

        public string MainPhone { get; set; }
    }
}

然后我将这个程序集注册到Dynamics 365中,如下:

Dynamics 365的Custom API介绍

 

下面就是与插件不同的地方,插件还需要Register New Step,而对于Custom API不需要,它需要的是打开对应的Custom API,将其Plugin Type设置为刚才注册Plugin Type,如下图,保存。

Dynamics 365的Custom API介绍

 

在调用之前我们可以去看看这个Customer API的metadata,打开类似的的 https://luoyongdemo.crm5.dynamics.com/api/data/v9.2/$metadata url可以看到,因为我们这个Custom API的Is Private为No,若是为Yes,则看不到,但是并不影响调用。

Dynamics 365的Custom API介绍

<FunctionImport Name="ly_GetAccountsByCityName" Function="Microsoft.Dynamics.CRM.ly_GetAccountsByCityName"/>

然后我通过Web API来调用它,因为是Function,所以是用Get的方法来调用,示例代码如下:我返回的数据是JSON格式,这里最好再parse一下就是JSON了。

var clientUrl = Xrm.Utility.getGlobalContext().getClientUrl();
var req = new XMLHttpRequest()
req.open("GET", clientUrl + "/api/data/v9.2/ly_GetAccountsByCityName(ly_CityName=@p1)?@p1='Redmond'", true);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.onreadystatechange = function () {
    if (this.readyState == 4) {
        req.onreadystatechange = null;
        if (this.status == 200) {
            var responseJSON = JSON.parse(this.responseText);
            console.log(responseJSON);
            var data =JSON.parse(responseJSON.ly_Accounts);
	    console.log(data);
        }
    }
}
req.send();

返回的示例如下:

{
 "@odata.context":"https://luoyongdemo.crm5.dynamics.com/api/data/v9.2/$metadata#Microsoft.Dynamics.CRM.ly_GetAccountsByCityNameResponse",
    "ly_Accounts":"[{\"MainPhone\":\"555-0155\",\"Name\":\"City Power & Light (sample)\"},{\"MainPhone\":\"555-0156\",\"Name\":\"Contoso Pharmaceuticals (sample)\"},{\"MainPhone\":\"555-0158\",\"Name\":\"A. Datum Corporation (sample)\"},{\"MainPhone\":\"+1-425-555-0120\",\"Name\":\"Fabrikam, Inc.\"},{\"MainPhone\":\"+1-425-555-3499\",\"Name\":\"Graphic Design Institute\"},{\"MainPhone\":\"425-555-0182\",\"Name\":\"A Datum Corporation\"},{\"MainPhone\":\"425-555-5816\",\"Name\":\"Contoso Engineering\"},{\"MainPhone\":\"425-555-9590\",\"Name\":\"Contoso Instrumentation\"},{\"MainPhone\":\"425-555-8536\",\"Name\":\"Contoso Pharma\"},{\"MainPhone\":\"425-555-0668\",\"Name\":\"Contoso Pharma Electronics\"}]"
}

 

如果Custom API设置的不是Function,也就是Is Function 字段的值设置为No呢,开发并没有多大不同我就不写例子了,使用Web API调用稍有不同,需要用Post来调用,我这里举个例子如下:

var clientUrl = Xrm.Utility.getGlobalContext().getClientUrl();
var req = new XMLHttpRequest()
req.open("POST", clientUrl + "/api/data/v9.2/ly_NonFunctionCustomAPI", true);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.onreadystatechange = function () {
    if (this.readyState == 4) {
        req.onreadystatechange = null;
        if (this.status == 200) {
            var responseJSON = JSON.parse(this.responseText);
            console.log(responseJSON);
            var data =JSON.parse(responseJSON.outputpara);
	    console.log(data);
        }
    }
}
var requestData={
   "para1":1
}
req.send(JSON.stringify(requestData));

 

不是Function的Custom API就可能是bounded action后者unbounded aciton,这个是可以通过Cloud Flow来调用。

开发完成了,但是解决方案中看不到这个Custom API及其相应输入输出参数,这时候需要通过解决方案中的 Add existing 将Custom API及其输入,输出参数添加进来,我这里添加后效果如下,记得要添加啊,不然通过解决方案部署不到新环境,这是目前的一个limit,相信很快可以解决。

Dynamics 365的Custom API介绍

如果通过组织服务来调用呢,我这里就摘录官方文档中的示例代码如下:

var req = new OrganizationRequest("myapi_EscalateCase")
{
  ["Target"] = new EntityReference("incident", guid),
  ["Priority"] = new OptionSetValue(1)
};
var resp = svc.Execute(req);
var newOwner = (EntityReference) resp["AssignedTo"];

你可能会问,可以禁用Custom API吗?答案是不可以,对Custom API执行Deactivate(禁用)操作没有效果,但是可以删除。

还有插件中调用不了Private类型的Custom API。

上一篇:Dynamics 365 在用工作流创建实体时,步骤一直提示缺少必需的属性


下一篇:Learning Latent Dynamics for Planning from Pixels