Dynamics 365 CE将自定义工作流活动程序集注册到磁盘并引用其他类库

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

本文参考了官方文档: Register a plug-in to be deployed on-premise 。

我们知道本地部署的Dynamics 365 Customer Engagement可以将自定义工作流活动程序集/插件程序集可以注册到非沙盒中(None) + 磁盘(Disk)中,这样注册有什么注意事项呢?一起来看看。

我这里使用如下的代码,准备在工作流或者操作中调用外部API:

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Workflow;
using Newtonsoft.Json.Linq;
using System;
using System.Activities;
using System.Net.Http;
using System.Net.Http.Headers;
using System.ServiceModel;
using System.Threading.Tasks;

namespace ActivitiesRegisteredInNoneDisk
{
    public class CallWebAPISample : CodeActivity
    {
        protected override void Execute(CodeActivityContext executionContext)
        {
            ITracingService tracingService = executionContext.GetExtension<ITracingService>();
            tracingService.Trace("Enter CallWebAPISample on {0}", DateTime.Now.ToString());
            IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();
            IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
            IOrganizationService orgService = serviceFactory.CreateOrganizationService(context.UserId);
            try
            {
                var responseContent = GetApiAsync("4292484973").Result;
                tracingService.Trace("HTTP POST RESPONSE CONTENT = {0}", responseContent);
                JObject jo = JObject.Parse(responseContent);
                throw new InvalidPluginExecutionException(jo["result"][0]["orderEntryDate"].ToString());
            }
            catch (FaultException<OrganizationServiceFault> ex)
            {
                tracingService.Trace(ex.Message + ex.StackTrace);
                throw new InvalidPluginExecutionException("CallWebAPISample encountered fault exception." + ex.Message);
            }
            catch (Exception e)
            {
                tracingService.Trace(e.Message + e.StackTrace);
                throw new InvalidPluginExecutionException("CallWebAPISample encountered general exception." + e.Message);
            }
            tracingService.Trace("Leave CallWebAPISample on {0}", DateTime.Now.ToString());
        }

        private async Task<string> GetApiAsync(string OrderNumber)
        {
            string returnVal = string.Empty;
            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                var response = client.GetAsync(string.Format("https://thomaswebapi.azurewebsites.net/api/Order?OrderNo={0}", OrderNumber)).Result;
                if (response.IsSuccessStatusCode)
                {
                    returnVal = await response.Content.ReadAsStringAsync();
                }
                else
                {
                    throw new Exception(response.Content.ToString());
                }
            }
            return returnVal;
        }
    }
}

为了展示引用其他类库,我这里特意引用了额外的类库Newtonsoft.Json,如下:

如果直接注册的话,我的注册界面如下,注意如果插件的isolation mode为 None,注册插件时候使用的账号除了需要据悉D365 CE的系统管理员角色外,需要属于部署管理员(Deployment Administrators)这个组:

这样注册很容易报错,报错如下:

报错的详细信息如下:

Unhandled Exception: System.ServiceModel.FaultException`1[[Microsoft.Xrm.Sdk.OrganizationServiceFault, Microsoft.Xrm.Sdk, Version=9.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]: Unable to load plug-in assembly.
Detail: <OrganizationServiceFault xmlns="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <ActivityId>9beef9f0-82da-4792-bb0e-48540c2a1d84</ActivityId>
  <ErrorCode>-2147204719</ErrorCode>
  <ErrorDetails xmlns:a="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
    <KeyValuePairOfstringanyType>
      <a:key>ApiExceptionSourceKey</a:key>
      <a:value i:type="b:string" xmlns:b="http://www.w3.org/2001/XMLSchema">Plugin/Microsoft.Crm.ObjectModel.PluginAssemblyService</a:value>
    </KeyValuePairOfstringanyType>
    <KeyValuePairOfstringanyType>
      <a:key>ApiOriginalExceptionKey</a:key>
      <a:value i:type="b:string" xmlns:b="http://www.w3.org/2001/XMLSchema">Microsoft.Crm.CrmException: Unable to load plug-in assembly. ---&gt; Microsoft.Crm.CrmException: Unable to load plug-in assembly.
   at Microsoft.Crm.ObjectModel.PluginAssemblyServiceInternal`1.LoadCrmPluginAssemblyMetadata(IBusinessEntity pluginAssembly, ExecutionContext context, Boolean loadAllMetadata)
   at Microsoft.Crm.ObjectModel.PluginAssemblyServiceInternal`1.RetrieveAssemblyMetadata(IBusinessEntity pluginAssembly, ExecutionContext context, Boolean retrieveFromExisting, Boolean forSystemAssembly)
   at Microsoft.Crm.ObjectModel.PluginAssemblyServiceInternal`1.ValidateOperation(String operationName, IBusinessEntity entity, ExecutionContext context)
   at Microsoft.Crm.ObjectModel.SdkEntityServiceBase.CreateInternal(IBusinessEntity entity, ExecutionContext context, Boolean verifyAction)
   --- End of inner exception stack trace ---
   at Microsoft.Crm.Extensibility.VersionedPluginProxyStepBase.Execute(PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.PipelineInstrumentationHelper.Execute(Boolean instrumentationEnabled, String stopwatchName, ExecuteWithInstrumentation action, PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.Pipeline.&lt;&gt;c__DisplayClass2_1.&lt;Execute&gt;b__0()</a:value>
    </KeyValuePairOfstringanyType>
    <KeyValuePairOfstringanyType>
      <a:key>ApiStepKey</a:key>
      <a:value i:type="b:guid" xmlns:b="http://schemas.microsoft.com/2003/10/Serialization/">3ecabb1b-ea3e-db11-86a7-000a3a5473e8</a:value>
    </KeyValuePairOfstringanyType>
    <KeyValuePairOfstringanyType>
      <a:key>ApiDepthKey</a:key>
      <a:value i:type="b:int" xmlns:b="http://www.w3.org/2001/XMLSchema">1</a:value>
    </KeyValuePairOfstringanyType>
    <KeyValuePairOfstringanyType>
      <a:key>ApiActivityIdKey</a:key>
      <a:value i:type="b:guid" xmlns:b="http://schemas.microsoft.com/2003/10/Serialization/">9beef9f0-82da-4792-bb0e-48540c2a1d84</a:value>
    </KeyValuePairOfstringanyType>
    <KeyValuePairOfstringanyType>
      <a:key>ApiPluginSolutionNameKey</a:key>
      <a:value i:type="b:string" xmlns:b="http://www.w3.org/2001/XMLSchema">System</a:value>
    </KeyValuePairOfstringanyType>
    <KeyValuePairOfstringanyType>
      <a:key>ApiStepSolutionNameKey</a:key>
      <a:value i:type="b:string" xmlns:b="http://www.w3.org/2001/XMLSchema">System</a:value>
    </KeyValuePairOfstringanyType>
  </ErrorDetails>
  <Message>Unable to load plug-in assembly.</Message>
  <Timestamp>2019-07-22T14:12:57.9063414Z</Timestamp>
  <ExceptionRetriable>false</ExceptionRetriable>
  <ExceptionSource i:nil="true" />
  <InnerFault>
    <ActivityId>9beef9f0-82da-4792-bb0e-48540c2a1d84</ActivityId>
    <ErrorCode>-2147204719</ErrorCode>
    <ErrorDetails xmlns:a="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
    <Message>Unable to load plug-in assembly.</Message>
    <Timestamp>2019-07-22T14:12:57.9063414Z</Timestamp>
    <ExceptionRetriable>false</ExceptionRetriable>
    <ExceptionSource i:nil="true" />
    <InnerFault i:nil="true" />
    <OriginalException i:nil="true" />
    <TraceText i:nil="true" />
  </InnerFault>
  <OriginalException i:nil="true" />
  <TraceText i:nil="true" />
</OrganizationServiceFault>

Server stack trace: 
   at System.ServiceModel.Channels.ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc)
   at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)
   at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
   at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)

Exception rethrown at [0]: 
   at Microsoft.Crm.Tools.Libraries.RegistrationHelper.RegisterAssembly(CrmOrganization org, String pathToAssembly, CrmPluginAssembly assembly)
   at Microsoft.Crm.Tools.AssemblyRegistration.PluginRegistrationViewModel.btnregisterClick()

开启CRM服务器端日志,如果包括了类似如下错误信息:

>Crm Exception: Message: Failed to load plugin assembly with exception System.IO.DirectoryNotFoundException: The system cannot find the path specified. (Exception from HRESULT: 0x80070003)
   at Microsoft.Crm.IMetaDataDispenserEx.OpenScope(String szScope, UInt32 dwOpenFlags, Guid& riid)
   at Microsoft.Crm.CrmPluginAssemblyMetadata.OpenScopeForAssemblyOnDisk(String fullFilePath)
   at Microsoft.Crm.CrmPluginAssemblyMetadata.LoadMetadataFromAssemblyFile(String fullFilePath, Boolean loadAllMetadata)
   at Microsoft.Crm.ObjectModel.PluginAssemblyServiceInternal`1.LoadCrmPluginAssemblyMetadata(IBusinessEntity pluginAssembly, ExecutionContext context, Boolean loadAllMetadata). PluginInfo => Crm plugin assembly info : sourcetype = 1, description = , ismanaged = False, pluginassemblyid = c21af9cf-0313-4af0-8c80-7d9644e4c581, ispasswordset = False, publickeytoken = 440C114085C1E28B, path = D:CodesThomasLuoCRMActivitiesRegisteredInNoneDiskinDebugActivitiesRegisteredInNoneDisk.dll, name = ActivitiesRegisteredInNoneDisk, culture = neutral, isolationmode = 1, version = 1.0.0.0, Location = D:CodesThomasLuoCRMActivitiesRegisteredInNoneDiskinDebugActivitiesRegisteredInNoneDisk.dll, ErrorCode: -2147220970, InnerException: System.IO.DirectoryNotFoundException: The system cannot find the path specified. (Exception from HRESULT: 0x80070003)
   at Microsoft.Crm.IMetaDataDispenserEx.OpenScope(String szScope, UInt32 dwOpenFlags, Guid& riid)
   at Microsoft.Crm.CrmPluginAssemblyMetadata.OpenScopeForAssemblyOnDisk(String fullFilePath)
   at Microsoft.Crm.CrmPluginAssemblyMetadata.LoadMetadataFromAssemblyFile(String fullFilePath, Boolean loadAllMetadata)
   at Microsoft.Crm.ObjectModel.PluginAssemblyServiceInternal`1.LoadCrmPluginAssemblyMetadata(IBusinessEntity pluginAssembly, ExecutionContext context, Boolean loadAllMetadata)

那就在自己的开发机器上简历和服务器安装目录一样的文件夹,比如我这里是在自己的开发机器上建立  C:Program FilesDynamics 365Serverinassembly 文件夹,然后将要部署的dll复制到这个文件夹中,注册程序集的时候从这个文件中中选择要注册的dll。还有需要将程序集DLL拷贝到CRM安装目录Serverinassembly 中了,我这里完整的路径是:C:Program FilesDynamics 365Serverinassembly 。我这里自定义工作流活动程序集文件名是ActivitiesRegisteredInNoneDisk.dll,我需要将这个文件复制到CRM前端,后端所有服务器的C:Program FilesDynamics 365Serverinassembly文件夹中。

还需要授予 everyone 账号对 C:Program FilesDynamics 365Serverinassembly 文件夹的Full control权限,根据 Unable to load Microsoft plug-in on a fresh CRM 9 on-premise organization 的解决方法,应该授予 Users 这个账号 Read & execute 权限即可,若这个权限足够的话就不要设置 everyone的权限了。记得改了以后重启前端服务器的IIS。

注册完了以后直接运行的话会报错:Assembly file name (ActivitiesRegisteredInNoneDisk.dll) is in invalid format. Only file name is allowed.

具体错误信息如下:

Unhandled Exception: Microsoft.Crm.CrmException: Assembly file name (ActivitiesRegisteredInNoneDisk.dll) is in invalid format. Only file name is allowed.
   at Microsoft.Crm.Extensibility.PluginAssemblyFactory.LoadAssembly(String assemblyFile)
   at Microsoft.Crm.Extensibility.PluginAssemblyFactory.LoadAssemblyWithoutMetadataValidation(PluginAssemblyDescription assemblyDescription, String assemblyName)
   at Microsoft.Crm.Extensibility.PluginAssemblyFactory.CreateInstance(Guid pluginAssemblyId, PluginAssemblyDescription& assemblyDescription, IOrganizationContext context)
   at Microsoft.Crm.Caching.PluginAssemblyCacheLoader.LoadCacheData(Guid key, ExecutionContext context)
   at Microsoft.Crm.Caching.ObjectModelCacheLoader`2.LoadCacheDataInternal(TKey key, Object existingDataContainer, IOrganizationContext context)
   at Microsoft.Crm.Caching.ObjectModelCacheLoader`2.LoadCacheData(TKey key, IOrganizationContext context)
   at Microsoft.Crm.Caching.CrmMultiOrgCacheBase`2.CreateEntry(TKey key, IOrganizationContext context)
   at Microsoft.Crm.Caching.CrmEntitySharedMultiOrgCache`2.LookupEntry(TKey key, IOrganizationContext context)
   at Microsoft.Crm.Caching.PluginTypeCacheLoader.LoadCacheData(Guid key, ExecutionContext context)
   at Microsoft.Crm.Caching.ObjectModelCacheLoader`2.LoadCacheDataInternal(TKey key, Object existingDataContainer, IOrganizationContext context)
   at Microsoft.Crm.Caching.ObjectModelCacheLoader`2.LoadCacheData(TKey key, IOrganizationContext context)
   at Microsoft.Crm.Caching.CrmMultiOrgCacheBase`2.CreateEntry(TKey key, IOrganizationContext context)
   at Microsoft.Crm.Caching.CrmEntitySharedMultiOrgCache`2.LookupEntry(TKey key, IOrganizationContext context)
   at Microsoft.Crm.Workflow.AsyncCustomActivityLoaderDirect.CustomActivityTypeAndAssemblyData(Guid pluginTypeId)
   at Microsoft.Crm.Workflow.Services.ActivityReferenceService.ResolveType(String assemblyQualifiedName)
   at Microsoft.Crm.Workflow.Services.ActivityReferenceService.InitializeActivity(ActivityReferenceBase activityReferenceBase)
   at Microsoft.Xrm.Sdk.Workflow.Activities.ActivityReferenceBase.CacheMetadata(NativeActivityMetadata metadata)
   at System.Activities.NativeActivity.OnInternalCacheMetadata(Boolean createEmptyBindings)
   at System.Activities.Activity.InternalCacheMetadata(Boolean createEmptyBindings, IList`1& validationErrors)
   at System.Activities.ActivityUtilities.ProcessActivity(ChildActivity childActivity, ChildActivity& nextActivity, Stack`1& activitiesRemaining, ActivityCallStack parentChain, IList`1& validationErrors, ProcessActivityTreeOptions options, ProcessActivityCallback callback)
   at System.Activities.ActivityUtilities.ProcessActivityTreeCore(ChildActivity currentActivity, ActivityCallStack parentChain, ProcessActivityTreeOptions options, ProcessActivityCallback callback, IList`1& validationErrors)
   at System.Activities.ActivityUtilities.CacheRootMetadata(Activity activity, LocationReferenceEnvironment hostEnvironment, ProcessActivityTreeOptions options, ProcessActivityCallback callback, IList`1& validationErrors)
   at System.Activities.Hosting.WorkflowInstance.ValidateWorkflow(WorkflowInstanceExtensionManager extensionManager)
   at System.Activities.Hosting.WorkflowInstance.RegisterExtensionManager(WorkflowInstanceExtensionManager extensionManager)
   at System.Activities.WorkflowApplication.EnsureInitialized()
   at System.Activities.WorkflowApplication.Enqueue(InstanceOperation operation, Boolean push)
   at System.Activities.WorkflowApplication.InternalRun(TimeSpan timeout, Boolean isUserRun)
   at System.Activities.WorkflowApplication.Run()
   at Microsoft.Crm.Workflow.ActivityHost.StartWorkflowExecution(Activity workflow, ICommonWorkflowContext context)
   at Microsoft.Crm.Workflow.ActivityHostBase.StartWorkflow(ICommonWorkflowContext context, Activity preLoadedActivity)

这个根据 Problems adding Step to Disk based Plugin Assembly 的说法,执行类似如下的SQL就可以了:

 update PluginAssemblybase set Path='ActivitiesRegisteredInNoneDisk.dll' 
 where Name ='ActivitiesRegisteredInNoneDisk'

这里还涉及到引用了外部程序集,需要将该程序集注册到CRM服务器的GAC。

服务器上没有 gacutil 的话,我这里从安装了 Visual Stuido 的我的机器上复制如下文件(文件夹)到服务器上:

C:Program Files (x86)Microsoft SDKsWindowsv10.0AinNETFX 4.6.2 Toolsgacutil.exe

C:Program Files (x86)Microsoft SDKsWindowsv10.0AinNETFX 4.6.2 Toolsgacutil.exe.config

C:Program Files (x86)Microsoft SDKsWindowsv10.0AinNETFX 4.6.2 Tools1033 文件夹中的所有文件

然后我用类似如下命令将其复制到GAC, gacutil 的使用请参考 Gacutil.exe (Global Assembly Cache Tool) 

cd C:Softwaregacutil
gacutil /i "C:Program FilesDynamics 365ServerinassemblyNewtonsoft.Json.dll" /f

然后我就可以顺利的调用Web API,并拿到结果了。

写了那么多,我只想说,不要将插件/自定义工作流程序集注册存储到Disk或者GAC中,而是遵从官方文档Register a plug-in to be deployed on-premise 的建议注册到Database中,官方原文是:We strongly recommend that you store your production-ready plug-ins in the Dynamics 365 for Customer Engagement apps database, instead of on-disk。

关于注册到数据库中相对与其他两种(磁盘和GAC)的优势,CRM 2015 – plugin deployment options 的说的比较简单明了,如下:

  1. The plugin is backed up when the database is backed up
  2. For multiple server configurations you only need to deploy once to the database and not individually to each CRM server.
  3. Plugins in the database can be added to solutions, Disk, GAC plugins cannot
  4. Plugins deployed to the GAC or Disk will need an IISRESET to refresh, plugins deployed in the database do not.
  5. Sandboxed and CRM Online plugins have to be deployed in the database
原文地址:https://www.cnblogs.com/luoyong0201/p/Dynamics_365_Register_Plugin_Workflowactivity_In_None_Disk.html