Dynamics 365-二:插件开发

参考文章

基本概念

D365平台与传统平台

​ D365平台与传统平台,在功能和页面开发层面均有不同之处。比如说在功能开发层面:传统平台需要自己开发,而365平台是系统标准的功能,不需要开发。在页面开发层面:传统平台需要自己开发,而365平台只需简单配置即可;

插件

Plugin(插件),它是一种事件处理程序,通过它可以修改或扩充Dynamics 365标准的业务流程,如创建时执行自定义逻辑。另外是SDK Message(SDK消息),通过它可以获取CRM标准功能中CreateUpdateDelete等相关操作的事件信息;

执行顺序

​ 有消息就会有事件先后的顺序,所以这边就会涉及两个名词,Pre-operationPost-operationPre-operation :消息事件触发前执行一个动作;Post-operation :消息事件触发后执行一个动作;

开发插件

1.新建VS解决方案项目类库

​ 首先需要创建一个 .NET FrameworkClass Library类库项目,这里要为不同版本的Dynamics365 选择的Framework不尽相同,请根据官方文档说明;

img

2.添加项目依赖包

​ 通过NuGet添加对Microsoft.CrmSdk.CoreAssemblies的引用,如下图,当然也要选择合适的版本。如果不能上网的话,就需要添加对 Microsoft.Xrm.Sdk.dllMicrosoft.Crm.Sdk.Proxy.dll 的引用;

img

3.新建插件

​ 插件文件本质也是类文件,只不过这个类继承IPlugin接口,且实现Execute方法,在此方法中编写插件代码,实现业务逻辑;建议:插件命名:执行时机+功能/作用英文+实体名+Plugin,例:CreateNew_StudentPlugin

​ 代码可以看到Execute方法只有一个输入参数serviceProvider,该参数的类型是IServiceProvider,是事件执行管道传递给当前插件的所有消息的容器,存储了在插件中可能要使用到的各类对象。通过IServiceProvider接口的GetService方法,可以获取执行上下文IPluginExecutionContext、组织服务工厂IOrganizationServiceFactory以及跟踪服务ITracingService等实例;

public void Execute(IServiceProvider serviceProvider)
{
    // 获取插件执行上下文
    IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

    // 获取组织服务工厂实例
    IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));

    // 获取组织服务实例
    IOrganizationService service = factory.CreateOrganizationService(context.UserId);

    // 获取跟踪服务
    ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

    try
    {
        // 插件业务逻辑代码
    }
    catch (FaultException<OrganizationServiceFault> ex)
    {
        // 异常处理代码
    }
}

4.插件签名

​ 在Visual Studio中右击该项目,选择属性(Properties) > 签名(Signing),选中 Sign the assembly,我这里新建一个Key file

img

Key file我的设置如下,为了简便,我就不设置密码保护了,保存后编译插件项目,确定没有编译错误

img

注册插件

1.下载注册工具

​ 从Dynamics 365 Customer Engagement (V9.0)开始,不再像以前一样提供SDK下载了,应该学习在线文档 中下载方式,以下下载方式主要是根据在线文章中命令方式下载:

本文提到下载的工具包括如下:

Tool NuGet Package
Code generation tool CrmSvcUtil.exe Microsoft.CrmSdk.CoreTools
Configuration Migration tool DataMigrationUtility.exe Microsoft.CrmSdk.XrmTooling.ConfigurationMigration.Wpf
Package Deployer PackageDeployer.exe Microsoft.CrmSdk.XrmTooling.PackageDeployment.WPF
Plug-in Registration Tool PluginRegistration.exe Microsoft.CrmSdk.XrmTooling.PluginRegistrationTool
SolutionPackager tool SolutionPackager.exe Microsoft.CrmSdk.CoreTools
  • 打开 Windows PowerShell,最好是以管理员身份打开

  • 切换到你要下载工具的目录

  • 执行如下的命令,执行完毕后记得回车

    $sourceNugetExe = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
    $targetNugetExe = ".
    uget.exe"
    Remove-Item .Tools -Force -Recurse -ErrorAction Ignore
    Invoke-WebRequest $sourceNugetExe -OutFile $targetNugetExe
    Set-Alias nuget $targetNugetExe -Scope Global -Verbose
    
    ##
    ##Download Plugin Registration Tool
    ##
    ./nuget install Microsoft.CrmSdk.XrmTooling.PluginRegistrationTool -O .Tools
    md .ToolsPluginRegistration
    $prtFolder = Get-ChildItem ./Tools | Where-Object {$_.Name -match 'Microsoft.CrmSdk.XrmTooling.PluginRegistrationTool.'}
    move .Tools$prtFolder	ools*.* .ToolsPluginRegistration
    Remove-Item .Tools$prtFolder -Force -Recurse
    
    ##
    ##Download CoreTools
    ##
    ./nuget install  Microsoft.CrmSdk.CoreTools -O .Tools
    md .ToolsCoreTools
    $coreToolsFolder = Get-ChildItem ./Tools | Where-Object {$_.Name -match 'Microsoft.CrmSdk.CoreTools.'}
    move .Tools$coreToolsFoldercontentincoretools*.* .ToolsCoreTools
    Remove-Item .Tools$coreToolsFolder -Force -Recurse
    
    ##
    ##Download Configuration Migration
    ##
    ./nuget install  Microsoft.CrmSdk.XrmTooling.ConfigurationMigration.Wpf -O .Tools
    md .ToolsConfigurationMigration
    $configMigFolder = Get-ChildItem ./Tools | Where-Object {$_.Name -match 'Microsoft.CrmSdk.XrmTooling.ConfigurationMigration.Wpf.'}
    move .Tools$configMigFolder	ools*.* .ToolsConfigurationMigration
    Remove-Item .Tools$configMigFolder -Force -Recurse
    
    ##
    ##Download Package Deployer 
    ##
    ./nuget install  Microsoft.CrmSdk.XrmTooling.PackageDeployment.WPF -O .Tools
    md .ToolsPackageDeployment
    $pdFolder = Get-ChildItem ./Tools | Where-Object {$_.Name -match 'Microsoft.CrmSdk.XrmTooling.PackageDeployment.Wpf.'}
    move .Tools$pdFolder	ools*.* .ToolsPackageDeployment
    Remove-Item .Tools$pdFolder -Force -Recurse
    
    ##
    ##Download Package Deployer PowerShell module
    ##
    ./nuget install Microsoft.CrmSdk.XrmTooling.PackageDeployment.PowerShell -O .Tools
    $pdPoshFolder = Get-ChildItem ./Tools | Where-Object {$_.Name -match 'Microsoft.CrmSdk.XrmTooling.PackageDeployment.PowerShell.'}
    move .Tools$pdPoshFolder	ools*.* .ToolsPackageDeployment.PowerShell
    Remove-Item .Tools$pdPoshFolder -Force -Recurse
    
    ##
    ##Remove NuGet.exe
    ##
    Remove-Item nuget.exe
    
  • 可以看到前面提到的工具都下载好了,一共是5个。如果要获取最新版本的工具,重复执行前面的步骤即可

    img

2.注册/附加插件

  • 打开插件注册工具

    image-20201130094139218

  • 登录-注册项目

    img

  • 新建插件

    点击菜单栏 新建(Register) 选项,依次点击新建插件(Register New Assembly)

    img

    按照示例图片步骤一中选择DLL文件,在步骤二中勾选添加的插件(类),最后点击下方添加

    img

  • 新建步骤(插件执行时机)

    点击菜单栏 新建(Register) 选项,依次点击新建步骤(Register New Step)

    img

    Message:执行时机,Primary Entity:关联实体,在什么时候执行

    img

调试插件

  • 安装调试工具

    img

  • 选中要调试的插件,选中调试的step,点击Start Profiling

    img

  • 去CRM中操作实体,下载日志文件

    img

  • 回到插件注册器关掉profiling

  • 点击Debug

    img

  • 到VS中,设置断点,点击调试=>附加PluginRegistration进程

    img

  • 再回到插件工具点击执行

    img

  • 这时就会看到断点生效,进行调试

    img

开启/配置实例镜像

打开注册工具,展开注册的插件项目,右键点击创建的执行项,点击 创建新镜像选项

镜像属性
镜像设置

开发技巧和问题

注册update插件获取entity踩过的坑

  • Update插件获取的entity只包含该条记录id和修改的字段值Create

  • 插件获取的entity包含该条记录的所有字段

注册delete插件获取entity踩过的坑

  • Update,Create插件获取的类型为entity

  • Delete 插件 获取的实体为entityreference

除了CUR以外还有那些事件可以注册?

  • Associate & Disassociate (N:N) 多对多关系;

  • Win & Lose 商机赢单和丢单

类型属性

IPluginExecutionContext

​ 插件执行上下文IPluginExecutionContext中,包括有事件处理管道传递给插件的各类信息,包括执行插件的运行时环境、执行管道相关信息以及触发Web服务的实体实例信息。IPluginExecutionContext接口中的成员列表如下所示:

名称 说明
ParentContext 从父管道操作中获取执行上下文信息。父子管道产生原因在于CRM系统中某些消息请求可能会产生其他消息请求。举例来说AssignRequest请求会产生一个UpdateRequest请求,如果两个插件A和U分别订阅了AssignRequest消息和UpdateRequest消息,那么在AssignRequest产生时,插件A、插件U将依次执行,此时插件U的执行上下文的ParentContext属性将被赋予插件A的执行上下文。
Stage 获取同步执行模式插件在执行管道中所处的阶段

IExecutionContext

IPluginExecutionContext接口继承自IExecutionContext接口,在IExecutionContext接口中,包含了大量的有关上下文的信息,如下表所示:

名称 说明
BusinessUnitId 获取执行管道所处理的实体实例的业务部门GUID。
CorrelationId 该属性的用途CRM平台为了避免出现无限死循环
Depth 获取当前插件在调用堆栈中的深度。也是为了避免出现无限死循环。插件开发人员可以在插件代码中对该属性进行判断从而避免出现无限死循环。经常的一种使用情景是,订阅了UpateRequest的插件代码中还执行了Update操作。
InitiatingUserId 获取当前执行事件管道的系统用户的GUID.
InputParameters 获取触发插件执行的请求消息参数.
IsExecutingOffline 获取当前插件是否运行在脱机环境中
IsInTransaction 获取插件是否执行在数据库事务中。
IsOfflinePlayback 如果插件可以运行在脱机环境中,那么在客户端与CRM服务器同步时,很可能由于数据同步造成服务器端同一插件再次执行一遍,从而导致数据出现了错误,此时,就需要在插件中使用本数据判断,是否是由于离线客户端与CRM服务器同步触发了本插件的运行。
IsolationMode 判断插件是否执行在沙盒Sandbox中。
MessageName 获取当前事件管道所处理的请求消息
Mode 获取插件执行模式:同步执行还是异步执行.
OperationCreatedOn 在与Azure云进行集成的时候用到的。
OperationId
OrganizationId 获取实体实例所属组织的GUID.
OrganizationName 获取实体实例所属组织的唯一名称.
OutputParameters 获取平台核心操作完成后的响应消息参数.
OwningExtension 获取相关联的SdkMessageProcessingingStep对象.
PostEntityImages 主要用于UpdateRequest消息中,分别获取核心操作之前以及之后实体的快照、映像信息
PreEntityImages
PrimaryEntityId 事件管道正在处理的实体实例的GUID
PrimaryEntityName 事件管道正在处理的实体的逻辑名称.
RequestId 事件管道正在处理的请求的GUID.
SharedVariables 获取/设置插件之间传递的自定义属性值.

CURD

提示:

  • 删除中注意targer是否可用

插件基本初始化类

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Client;
using System;

namespace T4
{
    public abstract class PluginBase : IPlugin
    {
        #region 相关服务
        //调试沙箱插件使用的跟踪服务
        protected ITracingService tracingservice;

        //插件的上下文
        protected IPluginExecutionContext context;

        //组织服务工厂
        protected IOrganizationServiceFactory serviceFactory;

        //组织服务
        protected IOrganizationService service;

        //组织服务
        protected IOrganizationService serviceAdmin;

        protected OrganizationServiceContext orgServiceContext;

        //相关记录
        protected Entity targer;
        protected Entity pretarger;
        protected Entity posttarger;

        protected EntityReference targerref;
        protected EntityReferenceCollection targerrefc;
        protected Relationship targerrel;

        protected EntityReference assignee;

        //触发操作
        protected bool isCreate;
        protected bool isUpdate;
        protected bool isDelete;
        protected bool isAssociate;
        protected bool isDisassociate;
        protected bool isAssign;

        #endregion

        public void Execute(IServiceProvider serviceProvider)
        {
            Initialize(serviceProvider);

            DoExecute(serviceProvider);
        }

        public abstract void DoExecute(IServiceProvider serviceProvider);



        /// <summary>
        /// 插件入口
        /// </summary>
        /// <param name="serviceProvider"></param>
        public void Initialize(IServiceProvider serviceProvider)
        {
            #region 相关服务的初始化
            tracingservice = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            // service = serviceFactory.CreateOrganizationService(context.UserId);
            service = serviceFactory.CreateOrganizationService(null);
            //service = serviceFactory.CreateOrganizationService(null);
            serviceAdmin = serviceFactory.CreateOrganizationService(null);
            //orgServiceContext = new OrganizationServiceContext(service);

            isCreate = context.MessageName == "Create";
            isUpdate = context.MessageName == "Update";
            isDelete = context.MessageName == "Delete";
            string contextMessage = context.MessageName;
            if (contextMessage == "Assign" || context.MessageName == "Associate" || context.MessageName == "Disassociate") return;


            if (context.InputParameters.Contains("Target"))
            {
                if (context.InputParameters["Target"] is Entity)
                    targer = (Entity)context.InputParameters["Target"];
                else if (context.InputParameters["Target"] is EntityReference)
                    targerref = (EntityReference)context.InputParameters["Target"];
            }


            #endregion
        }

        /// <summary>
        /// 插件入口
        /// </summary>
        /// <param name="serviceProvider"></param>
        public void InitializeAssociate(IServiceProvider serviceProvider)
        {
            #region 相关服务的初始化
            tracingservice = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            service = serviceFactory.CreateOrganizationService(context.UserId);
            serviceAdmin = serviceFactory.CreateOrganizationService(null);
            orgServiceContext = new OrganizationServiceContext(service);

            isAssociate = context.MessageName == "Associate";
            isDisassociate = context.MessageName == "Disassociate";

            targerref = (EntityReference)context.InputParameters["Target"];
            targerrefc = (EntityReferenceCollection)context.InputParameters["RelatedEntities"];
            targerrel = (Relationship)context.InputParameters["Relationship"];

            #endregion
        }

        /// <summary>
        /// 插件入口
        /// </summary>
        /// <param name="serviceProvider"></param>
        public void InitializeAssign(IServiceProvider serviceProvider)
        {
            #region 相关服务的初始化
            tracingservice = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            service = serviceFactory.CreateOrganizationService(context.UserId);
            serviceAdmin = serviceFactory.CreateOrganizationService(null);
            orgServiceContext = new OrganizationServiceContext(service);
            //tracingservice.Trace("相关服务的初始化");
            isAssign = context.MessageName == "Assign";

            //tracingservice.Trace("相关对象初始化Target");
            targerref = (EntityReference)context.InputParameters["Target"];
            //tracingservice.Trace("相关对象初始化Assignee");
            assignee = (EntityReference)context.InputParameters["Assignee"];
            //tracingservice.Trace("相关对象初始化 ok");
            #endregion
        }

    }
}

新建插件

提示:

  • 日期类型为:DateTime类型
  • 货币类型为:Money类型
  • 选项集类型为:OptionSetValue类型
  • 查找类型为:EntityReference类型
  • 实体字段名区分大小写
using Microsoft.Xrm.Sdk;
using System;
namespace T4
{
    public class CreatePlugin : PluginBase
    {
        public override void DoExecute(IServiceProvider serviceProvider)
        {
            if (context.Depth > 1) return; 
            
			// 示例一
            Random random = new Random();
            Entity entity = new Entity(targer.LogicalName);
            entity["new_age"] = random.Next(18, 99);
            entity["new_name"] = $"学员:"+ random.Next(1000,9999);
            entity["new_gender"] = true;
            EntityReference entityReference = new EntityReference(targer.LogicalName, Guid.Parse("2867B927-B12F-EB11-B392-005056993F73"));
            entity["new_search"] = entityReference;
            entity["new_admissiondate"] = DateTime.UtcNow;
            
            service.Create(entity);
            
            // 示例二
            Entity entitys = new Entity(targer.LogicalName);
            entitys["new_name"] = "YH";//单行文本类型
            entitys["new_client_name"] = "Hello Word";//单行文本类型
            entitys["new_client_id"] = 123456;//整形
            entitys["new_client_float"] = 54.8;//浮点类型
            entitys["new_client_sex"] = false;//两个选择
            entitys["new_client_addtime"] = DateTime.Parse("2019-1-1 12:30:12");//日期类型
            entitys["new_client_money"] = new Money(3000);//货币类型
            entitys["new_client_cardnumber"] = decimal.Parse("50000");//十进制类型
            entitys["new_client_summary"] = "全力以赴";//多行文本类型
            entitys["new_client_a"] = new OptionSetValue(100000000);//单项选项集
            entitys["new_client_select"] = new EntityReference(targer.LogicalName, Guid.Parse("596B44E4-AF24-EB11-956F-E03F49115DFE"));//查找类型

            service.Create(entitys);
        }
    }
}

修改插件

提示:

  • 修改时如需使用修改前后实体镜像,需要插件注册工具中开启(配置)
using Microsoft.Xrm.Sdk;
using System;
namespace T4
{
    public class UpdatePlugin : PluginBase
    {
        public override void DoExecute(IServiceProvider serviceProvider)
        {
            if (context.Depth > 1) return;
            
            // 获取修改后实体镜像(修改后数据)
            posttarger = context.PostEntityImages["image"];
            // 获取修改前实体镜像(修改前数据)
            pretarger = context.PreEntityImages["image"];

            //判断字段是否存在
            if (!posttarger.Contains("new_name")) return;

            Random random = new Random();
            posttarger["new_age"] = random.Next(18, 99);
            posttarger["new_name"] = $"学员:" + random.Next(1000, 9999);
            posttarger["new_gender"] = true;

            // 根据实体名称,id创建查找类型值
            EntityReference entityReference = new EntityReference(posttarger.LogicalName, Guid.Parse("2867B927-B12F-EB11-B392-005056993F73"));
            posttarger["new_search"] = entityReference;

            // 返回实体查找类型字段值,值为 EntityReference 类型
            var entityReference = posttarger.GetAttributeValue<EntityReference>("new_search");

            service.Update(posttarger);
        }
    }
}

查询插件

// 单查询
var entity = service.Retrieve("实体名", Guid.NewGuid(), new Microsoft.Xrm.Sdk.Query.ColumnSet(true));

// 多查询
// 1.创建查询表达式,并执行查询实体
QueryExpression query = new QueryExpression(context.PrimaryEntityName);
// 2.查询显示列名集合,true为全部显示
query.ColumnSet = new ColumnSet(true);
// 2.条件筛选,格式:字段名,表达式,条件值
query.Criteria.AddCondition("字段名", ConditionOperator.Equal, "值");
// 3.执行查询
var collection = service.RetrieveMultiple(query);
// 4.获取实体集合
var entity_list = collection.Entities;

// 3.FetchXML查询
private Entity GetSequenceEntity(IOrganizationService service, Guid code)
{
    var fetchXml = $@"
        <fetch version='1.0' output-format='xml-platform' mapping='logical'  distinct='false'>
          <entity name='mcs_tc_order'>
            <attribute name='jk_lockstatus'/>
            <filter type='and'>
                <condition attribute='mcs_tc_orderid' operator='eq'  value='{code}' />
            </filter>
          </entity>
        </fetch>";
    var response = service.RetrieveMultiple(new FetchExpression(fetchXml));
    if (response != null && response.Entities.Count > 0)
    {
        var entity = response.Entities[0];
        return entity;
    }
    return null;
}

删除插件

service.Delete(targer.LogicalName, targer.Id);// 当前实体名和实体名id
到达胜利之前无法回头!
原文地址:https://www.cnblogs.com/weiyongguang/p/14060783.html