我是微软Dynamics 365 & Power Platform方面的工程师/顾问罗勇,也是2015年7月到2018年6月连续三年Dynamics CRM/Business Solutions方面的微软最有价值专家(Microsoft MVP),欢迎关注我的微信公众号 MSFTDynamics365erLuoYong ,回复464或者20220307可方便获取本文,同时可以在第一间得到我发布的最新博文信息,follow me!
前面的文章 通过Custom Connector获取flow的运行记录 讲述了通过flow获取flow的运行记录,这篇文章扩展下,我们做个配置表出来,配置监控哪些flow,以及实际将flow的运行记录主要信息放到Microsoft Dataverse中。
首先我简历一个配置Flow监控的表,状态为可用的就是要监控的,停用的记录就不再监控,这样利用标准的启用/停用功能来配置是否监控。还做了个Flow运行记录表,用来存储flow运行记录的主要信息。
然后建立一个环境变量 (了解环境变量可以参考我的博文 在Power Apps中使用环境变量 )来存储一些配置性的东西,如下:
保存后打开这个环境变量,设置它的默认值(Default Value)如下:
{
"EnvironmentId":"6cd1dc1a-56f7-4090-ba7c-e06c4055c81a",
"DefaultSolutionId":"fd140aaf-4df4-11dd-bd17-0019b9312238",
"AlertEmails":"[email protected]",
"ViewFlowLink":"https://asia.flow.microsoft.com/manage/environments/{0}/solutions/{1}/flows/{2}/",
"ViewFlowRunHistoryLink":"https://asia.flow.microsoft.com/manage/environments/{0}/solutions/{1}/flows/{2}/runs/{3}"
}
然后我创建一个Custom API来批量创建和设置Flow设置记录:
这个Custom API我使用的代码如下:
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.Text;
namespace D365.Plugins
{
public class CustomAPIUpsertFlowMonitorSettings : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService adminOrgSvc = serviceFactory.CreateOrganizationService(null);
var flowSetting = GetEnvironmentVariableValue("ly_FlowMonitorSetting", adminOrgSvc, tracingService);
if (string.IsNullOrEmpty(flowSetting))
{
throw new InvalidPluginExecutionException("名称为ly_FlowMonitorSetting的环境变量没有设置,请联系系统管理员!");
}
var flowSettingDic = DeserializeDictionary(flowSetting);
if (!flowSettingDic.ContainsKey("EnvironmentId") || !flowSettingDic.ContainsKey("DefaultSolutionId") || !flowSettingDic.ContainsKey("AlertEmails") || !flowSettingDic.ContainsKey("ViewFlowLink") || !flowSettingDic.ContainsKey("ViewFlowRunHistoryLink"))
{
throw new InvalidPluginExecutionException("名称为ly_FlowMonitorSetting的环境变量设置有问题,部分元素没有设置,请联系系统管理员!");
}
var queryWorkflow = new QueryExpression("workflow");
queryWorkflow.NoLock = true;
queryWorkflow.ColumnSet = new ColumnSet("name", "workflowidunique", "statecode");
queryWorkflow.Criteria.AddCondition("category", ConditionOperator.Equal, 5);
queryWorkflow.Criteria.AddCondition("type", ConditionOperator.Equal, 1);
var workflowEc = adminOrgSvc.RetrieveMultiple(queryWorkflow);
QueryExpression qe;
EntityCollection ec;
foreach (var workflowEntity in workflowEc.Entities)
{
qe = new QueryExpression("ly_flowmonitorsetting");
qe.TopCount = 1;
qe.NoLock = true;
qe.ColumnSet.AddColumn("ly_flowmonitorsettingid");
qe.Criteria.AddCondition("ly_workflowidunique", ConditionOperator.Equal, workflowEntity.GetAttributeValue<Guid>("workflowidunique").ToString());
ec = adminOrgSvc.RetrieveMultiple(qe);
if (ec.Entities.Any())
{
var updateEntity = new Entity("ly_flowmonitorsetting", ec.Entities[0].Id);
updateEntity["ly_flowname"] = workflowEntity.GetAttributeValue<string>("name");
updateEntity["ly_isturnon"] = workflowEntity.GetAttributeValue<OptionSetValue>("statecode").Value == 0 ? false : true;
adminOrgSvc.Update(updateEntity);
}
else
{
var createEntity = new Entity("ly_flowmonitorsetting");
createEntity["ly_name"] = workflowEntity.GetAttributeValue<string>("name");
createEntity["ly_isturnon"] = workflowEntity.GetAttributeValue<OptionSetValue>("statecode").Value == 0 ? false : true;
createEntity["ly_workflowidunique"] = workflowEntity.GetAttributeValue<Guid>("workflowidunique").ToString();
createEntity["ly_flowname"] = workflowEntity.GetAttributeValue<string>("name");
createEntity["ly_viewflowlink"] = string.Format(flowSettingDic["ViewFlowLink"], flowSettingDic["EnvironmentId"], flowSettingDic["DefaultSolutionId"], workflowEntity.GetAttributeValue<Guid>("workflowidunique").ToString());
adminOrgSvc.Create(createEntity);
}
}
}
private string GetEnvironmentVariableValue(string variableSchemaName, IOrganizationService service, ITracingService tracingService)
{
if (string.IsNullOrEmpty(variableSchemaName))
{
throw new InvalidPluginExecutionException("环境变量名称为空,中止查询该变量的值!");
}
string returnVal = string.Empty;
var fetchXml = string.Format(@"<fetch versinotallow='1.0' mapping='logical' top='1' no-lock='true'>
<entity name='environmentvariabledefinition'>
<attribute name='defaultvalue'/>
<filter type='and'>
<condition attribute='schemaname' operator='eq' value='{0}'/>
<condition attribute='statecode' operator='eq' value='0'/>
</filter>
<link-entity name='environmentvariableval(new FetchExpression(fetchXml));
if (ec.Entities.Count >= 1)
{
if (ec.Entities[0].Contains("varvalue.value"))
{
returnVal = ec.Entities[0].GetAttributeValue<AliasedValue>("varvalue.value").Value.ToString();
}
else
{
returnVal = ec.Entities[0].GetAttributeValue<string>("defaultvalue");
}
}
tracingService.Trace($"名称为{variableSchemaName}环境变量的值是{returnVal}");
return returnVal;
}
private Dictionary<string, string> DeserializeDictionary(string josnStr)
{
Dictionary<string, string> returnVal = null;
if (!string.IsNullOrEmpty(josnStr))
{
DataContractJsonSerializerSettings serializerSettings = new DataContractJsonSerializerSettings
{
UseSimpleDictionaryFormat = true
};
using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(josnStr)))
{
DataContractJsonSerializer deseralizer = new DataContractJsonSerializer(typeof(Dictionary<string, string>), serializerSettings);
returnVal = (Dictionary<string, string>)deseralizer.ReadObject(ms);
}
}
return returnVal;
}
}
}
然后在Flow监控配置的实体的列表界面我做了一个按钮,使用的调用代码如下:
"use strict";
var LY = window.LY || {};
LY.flowmonitorsettingRibbon = LY.flowmonitorsettingRibbon || {};
(function () {
this.bulkUpsertRecords = function (selectedControl) {
Xrm.Utility.showProgressIndicator("处理中,请稍候...");
var clientUrl = Xrm.Utility.getGlobalContext().getClientUrl();
var req = new XMLHttpRequest();
req.open("POST", `${clientUrl}/api/data/v9.2/ly_UpsertFlowMonitorSettings`, 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 == 204) {
Xrm.Utility.closeProgressIndicator();
selectedControl.refresh();
}
else {
Xrm.Utility.closeProgressIndicator();
selectedControl.refresh();
var error = JSON.parse(this.response).error;
Xrm.Navigation.openErrorDialog({ message: error.message });
}
}
}
req.send();
}
}).call(LY.flowmonitorsettingRibbon);
//# flowmonitorsetting_ribbon.js
这个按钮涉及到的RibbonDiff内容如下:
<RibbonDiffXml>
<CustomActions>
<CustomAction Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.Button.CustomAction" Location="Mscrm.HomepageGrid.ly_flowmonitorsetting.MainTab.Management.Controls._children" Sequence="55">
<CommandUIDefinition>
<Button Command="ly.ly_flowmonitorsetting.bulkUpsertRecords.Command" Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.Button" LabelText="$LocLabels:ly.ly_flowmonitorsetting.bulkUpsertRecords.Button.LabelText" Sequence="55" TemplateAlias="o2" ToolTipDescription="$LocLabels:ly.ly_flowmonitorsetting.bulkUpsertRecords.Button.ToolTipDescription" ModernImage="New" />
</CommandUIDefinition>
</CustomAction>
</CustomActions>
<Templates>
<RibbonTemplates Id="Mscrm.Templates"></RibbonTemplates>
</Templates>
<CommandDefinitions>
<CommandDefinition Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.Command">
<EnableRules>
<EnableRule Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.EnableRule" />
</EnableRules>
<DisplayRules />
<Actions>
<JavaScriptFunction FunctionName="LY.flowmonitorsettingRibbon.bulkUpsertRecords" Library="$webresource:ly_/script/flowmonitorsetting/flowmonitorsetting_ribbon.js">
<CrmParameter Value="SelectedControl" />
</JavaScriptFunction>
</Actions>
</CommandDefinition>
</CommandDefinitions>
<RuleDefinitions>
<TabDisplayRules />
<DisplayRules />
<EnableRules>
<EnableRule Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.EnableRule">
<SelectionCountRule AppliesTo="PrimaryEntity" Minimum="0" Maximum="250" Default="false" InvertResult="false" />
</EnableRule>
</EnableRules>
</RuleDefinitions>
<LocLabels>
<LocLabel Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.Button.LabelText">
<Titles>
<Title description="创建/更新配置" languagecode="1033" />
</Titles>
</LocLabel>
<LocLabel Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.Button.ToolTipDescription">
<Titles>
<Title description="自动获取Flow信息,创建/更新Flow监控配置信息" languagecode="1033" />
</Titles>
</LocLabel>
</LocLabels>
</RibbonDiffXml>
然后我点击按钮就将现有的Cloud Flow的信息插入/更新到Flow监控配置实体中了。然后我将其他flow监控记录停用,只保留我自己的这个flow用于演示。
我再做个定期运行的flow用来刷新flow的信息,获取flow的运行记录。
为Flow起个名字,点击Create按钮
根据需要设置运行频率:
为了避免表达式太长,我声明三个变量来存储环境变量中的值:
使用的表达式类似 parameters('FlowMonitorSetting (ly_FlowMonitorSetting)')['DefaultSolutionId']
然后获取需要监控flow的基本信息,步骤如下:
使用的FetchXml是
<fetch mapping="logical" distinct="false" no-lock="true">
<entity name="workflow">
<attribute name="workflowid" />
<attribute name="statecode" />
<attribute name="workflowidunique" />
<filter type="and">
<condition attribute="category" operator="eq" value="5" />
<condition attribute="type" operator="eq" value="1" />
</filter>
<link-entity name="ly_flowmonitorsetting" from="ly_workflowidunique" to="workflowidunique" link-type="inner" alias="setting">
<filter type="and">
<condition attribute="statecode" operator="eq" value="0" />
</filter>
</link-entity>
</entity>
</fetch>
对于每个监控的flow,我先根据Workflow的唯一ID去找到Flow监控配置记录,然后更新其开关状态字段,用到的主要表达式是
ly_workflowidunique eq '@{items('Apply_to_each')?['workflowidunique']}' and statecode eq 0
if(equals(items('Apply_to_each')?['statecode'],0),false,true)
然后获取监控的每个flow的运行记录,并循环每个运行记录,看是否已经存在运行记录唯一ID相同的记录,用到的主要表达式如下:
@{parameters('FlowMonitorSetting (ly_FlowMonitorSetting)')['EnvironmentId']}
@{items('Apply_to_each')?['workflowidunique']}
body('Get_Flow_Run_History')['value']
如果没有就要新增flow运行记录:
用到的主要表达式如下,有写地方用了if,因为如果flow运行成功的话,有些元素是没有的,如果不管有没有都去读取的话,flow运行会报错。
length(outputs('查找相同运行id的flow运行记录')?['body/value'])
/ly_flowmonitorsettings(@{items('Apply_to_each_2')?['ly_flowmonitorsettingid']})
@{replace(replace(replace(replace(variables('ViewFlowRunHistoryLink'),'{0}',variables('EnvironmentId')),'{1}',variables('DefaultSolutionId')),'{2}',items('Apply_to_each')?['workflowidunique']),'{3}',items('Apply_to_each_Flow_Run_History')['name'])}
@{items('Apply_to_each_Flow_Run_History')['properties/startTime']}
@{items('Apply_to_each_Flow_Run_History')['properties/status']}
@{items('Apply_to_each_Flow_Run_History')['properties/endTime']}
@{if(equals(items('Apply_to_each_Flow_Run_History')['properties/status'],'Succeeded'),'',items('Apply_to_each_Flow_Run_History')['properties/code'])}
@{items('Apply_to_each_Flow_Run_History')['name']}
@{if(equals(items('Apply_to_each_Flow_Run_History')['properties/status'],'Succeeded'),'',items('Apply_to_each_Flow_Run_History')['properties/error/code'])}
@{if(equals(items('Apply_to_each_Flow_Run_History')['properties/status'],'Succeeded'),'',items('Apply_to_each_Flow_Run_History')['properties/error/message'])}
效果如下:
获取到的错误代码、错误消息不一定是显示详细错误信息,此处有待改进。
获取的其他信息我得测试结果表明是准确的。