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

我们发布解决方案到目标环境,希望发布的内容尽量精准,发布过去的组件尽量只是需要发布的,不要带其他不需要发布的组件,如果要检查呢?一个个手工比较太Low。我目前没有找到很好的办法,根据项目的实践,一般容易造成问题的主要是实体以及它的组件,我就准备了一个程序,读取导出解决方案的 customizations.xml 内容和要导入的目标环境的实体元数据进行对比,然后将对比结果生成Excel方便分析和比较。

关于查询实体元数据可以参考我前面的博文:

程序使用的主要代码如下:

using Microsoft.Xrm.Tooling.Connector;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.Office.Interop.Excel;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata;
using System.IO;
using Microsoft.Xrm.Sdk.Query;

namespace CompareComponents
{
class Program
{
public static string[] excludeColumns = "ownerid,owningbusinessunit,owningteam,owninguser,statecode,modifiedby,modifiedbyexternalparty,modifiedon,modifiedonbehalfby,overriddencreatedon,createdby,createdbyexternalparty,createdon,createdonbehalfby,entityimage".Split(',');
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.GetEncoding("gb2312");
Console.WriteLine("本程序用于读取解决方案中的customizations.xml中内容和要布署的目标环境组件进行对比!,开始使用请输入Y!");
var inputValue = Console.ReadLine();
if (inputValue.Equals("Y", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("开始读取customizations.xml文件中内容!");
var customizationsFilePath = ConfigurationManager.AppSettings["CustomizationsFilePath"];
List<TableMetadata> lsTables = new List<TableMetadata>();
XElement componentstoDeploy = XElement.Load(customizationsFilePath);
IEnumerable<XElement> tableEles = from item in componentstoDeploy.Descendants("Entities").FirstOrDefault().Descendants("Entity")
select item;
foreach(var tableEle in tableEles)
{
var table = new TableMetadata();
table.TableDispalyName = tableEle.Element("Name").Attribute("LocalizedName").Value.ToString();
table.TableSchemaName = tableEle.Element("Name").Value.ToString();
table.Columns = new List<ColumnMetadata>();
table.Forms = new List<FormMetadata>();
table.SavedQueries = new List<SavedQueryMetadata>();
table.Visualizations = new List<VisualizationMetadata>();

//处理列
if (tableEle.Descendants("attributes").Any())
{
var columnEles = from item in tableEle.Descendants("attributes").FirstOrDefault().Descendants("attribute")
select item;
foreach (var columnEle in columnEles)
{
var columnLogicalName = columnEle.Attribute("PhysicalName").Value.ToString().ToLower();
var columnDataType = columnEle.Element("Type").Value.ToString();
if (!excludeColumns.Contains(columnLogicalName) && !columnDataType.Equals("primarykey", StringComparison.OrdinalIgnoreCase))
table.Columns.Add(new ColumnMetadata()
{
LogicalName = columnLogicalName,
DataType = ChangeColumnType(columnDataType)
});
}
}

//处理表单
if (tableEle.Element("FormXml") != null)
{
var formEles = from item in tableEle.Element("FormXml").Descendants("forms")
select item;
foreach (var formEle in formEles)
{
foreach (var systemfrom in formEle.Descendants("systemform"))
{
table.Forms.Add(new FormMetadata()
{
Name = systemfrom.Element("LocalizedNames").Descendants("LocalizedName").FirstOrDefault().Attribute("description").Value.ToString(),
FormId = Guid.Parse(systemfrom.Element("formid").Value.ToString())
});
}
}
}

//处理公共视图
if (tableEle.Element("SavedQueries") != null)
{
var savedqueryEles = from item in tableEle.Element("SavedQueries").Element("savedqueries").Descendants("savedquery")
select item;
foreach (var savedqueryEle in savedqueryEles)
{
table.SavedQueries.Add(new SavedQueryMetadata()
{
Name = savedqueryEle.Element("LocalizedNames").Descendants("LocalizedName").FirstOrDefault().Attribute("description").Value.ToString(),
SavedQueryId = Guid.Parse(savedqueryEle.Element("savedqueryid").Value.ToString())
});
}
}

//处理实体公共图表
if (tableEle.Element("Visualizations") != null) {
var visualizationEles = from item in tableEle.Element("Visualizations").Descendants("visualization")
select item;
foreach (var visualizationEle in visualizationEles)
{
table.Visualizations.Add(new VisualizationMetadata()
{
Name = visualizationEle.Element("LocalizedNames").Descendants("LocalizedName").FirstOrDefault().Attribute("description").Value.ToString(),
SavedQueryVisualizationId = Guid.Parse(visualizationEle.Element("savedqueryvisualizationid").Value.ToString())
});
}
}

lsTables.Add(table);
}
Console.WriteLine("完成读取customizations.xml文件中内容!");
var excelApp = new Application();
excelApp.Visible = false;
Workbook deployWorkbook = excelApp.Workbooks.Add();
Worksheet deployWorksheet = (Worksheet)excelApp.ActiveSheet;
deployWorksheet.Name = "布署信息";
int row = 1;
deployWorksheet.Cells[1, 1] = "表名";
deployWorksheet.Cells[1, 2] = "表架构名";
deployWorksheet.Cells[1, 3] = "组件类型";
deployWorksheet.Cells[1, 4] = "组件名";
deployWorksheet.Cells[1, 5] = "组件ID/字段类型";
deployWorksheet.Cells[1, 6] = "布署类型";
deployWorksheet.Cells[1, 7] = "说明";
deployWorksheet.Rows[1].Font.Bold = true;//字体加粗
row++;
try
{
Console.WriteLine($"开始连接到指定的Dynamics 365环境");
CrmServiceClient crmServiceClient = new CrmServiceClient(ConfigurationManager.AppSettings["connectStr"]);
if (crmServiceClient.IsReady)
{
Console.WriteLine($"连接到Dynamics 365环境 ({crmServiceClient.ConnectedOrgFriendlyName}) 成功");
foreach (var table in lsTables)
{
try
{
RetrieveEntityRequest request = new RetrieveEntityRequest
{
EntityFilters = EntityFilters.All,
LogicalName = table.TableSchemaName.ToLower(),
RetrieveAsIfPublished = true
};
RetrieveEntityResponse response = crmServiceClient.Execute(request) as RetrieveEntityResponse;
var attributes = response.EntityMetadata.Attributes;
//处理列
foreach (var column in table.Columns)
{
deployWorksheet.Cells[row, 1] = table.TableDispalyName;
deployWorksheet.Cells[row, 2] = table.TableSchemaName;
deployWorksheet.Cells[row, 3] = "字段";
deployWorksheet.Cells[row, 4] = column.LogicalName;
deployWorksheet.Cells[row, 5] = column.DataType;
var findColumn = attributes.Where(t => t.LogicalName.Equals(column.LogicalName,StringComparison.OrdinalIgnoreCase));
if (findColumn.Any())
{
deployWorksheet.Cells[row, 6] = "修改";
if (!findColumn.First().AttributeTypeName.Value.Equals(column.DataType, StringComparison.OrdinalIgnoreCase))
{
deployWorksheet.Cells[row, 7] = $"字段类型由原来的{findColumn.First().AttributeTypeName.Value}要变成{column.DataType}";
}
}
else
{
deployWorksheet.Cells[row, 6] = "新增";
}
row++;
}

//处理表单
var systemFormsQuery = new QueryExpression("systemform");
systemFormsQuery.NoLock = true;
systemFormsQuery.ColumnSet.AddColumn("formid");
systemFormsQuery.Criteria.AddCondition("objecttypecode",ConditionOperator.Equal,table.TableSchemaName.ToLower());
var systemForms = crmServiceClient.RetrieveMultiple(systemFormsQuery);
foreach (var form in table.Forms)
{
deployWorksheet.Cells[row, 1] = table.TableDispalyName;
deployWorksheet.Cells[row, 2] = table.TableSchemaName;
deployWorksheet.Cells[row, 3] = "表单";
deployWorksheet.Cells[row, 4] = form.Name;
deployWorksheet.Cells[row, 5] = form.FormId.ToString();
var findForm = systemForms.Entities.Where(t => t.Id.Equals(form.FormId));
if (findForm.Any())
{
deployWorksheet.Cells[row, 6] = "修改";
}
else
{
deployWorksheet.Cells[row, 6] = "新增";
}
row++;
}

//处理公共视图
var savedQueriesQuery = new QueryExpression("savedquery");
savedQueriesQuery.NoLock = true;
savedQueriesQuery.ColumnSet.AddColumn("savedqueryid");
savedQueriesQuery.Criteria.AddCondition("returnedtypecode", ConditionOperator.Equal, table.TableSchemaName.ToLower());
var savedQueries = crmServiceClient.RetrieveMultiple(savedQueriesQuery);
foreach (var savedquery in table.SavedQueries)
{
deployWorksheet.Cells[row, 1] = table.TableDispalyName;
deployWorksheet.Cells[row, 2] = table.TableSchemaName;
deployWorksheet.Cells[row, 3] = "视图";
deployWorksheet.Cells[row, 4] = savedquery.Name;
deployWorksheet.Cells[row, 5] = savedquery.SavedQueryId.ToString();
var findSavedQueries = savedQueries.Entities.Where(t => t.Id.Equals(savedquery.SavedQueryId));
if (findSavedQueries.Any())
{
deployWorksheet.Cells[row, 6] = "修改";
}
else
{
deployWorksheet.Cells[row, 6] = "新增";
}
row++;
}

//处理公共图表
var visualizationsQuery = new QueryExpression("savedqueryvisualization");
visualizationsQuery.NoLock = true;
visualizationsQuery.ColumnSet.AddColumn("savedqueryvisualizationid");
visualizationsQuery.Criteria.AddCondition("primaryentitytypecode", ConditionOperator.Equal, table.TableSchemaName.ToLower());
var visualizations = crmServiceClient.RetrieveMultiple(visualizationsQuery);
foreach (var visualization in table.Visualizations)
{
deployWorksheet.Cells[row, 1] = table.TableDispalyName;
deployWorksheet.Cells[row, 2] = table.TableSchemaName;
deployWorksheet.Cells[row, 3] = "图表";
deployWorksheet.Cells[row, 4] = visualization.Name;
deployWorksheet.Cells[row, 5] = visualization.SavedQueryVisualizationId.ToString();
var findVisualizations = visualizations.Entities.Where(t => t.Id.Equals(visualization.SavedQueryVisualizationId));
if (findVisualizations.Any())
{
deployWorksheet.Cells[row, 6] = "修改";
}
else
{
deployWorksheet.Cells[row, 6] = "新增";
}
row++;
}
}
catch (Exception ex)
{
deployWorksheet.Cells[row, 1] = table.TableDispalyName;
deployWorksheet.Cells[row, 2] = table.TableSchemaName;
deployWorksheet.Cells[row, 3] = "表";
deployWorksheet.Cells[row, 6] = "新增";
deployWorksheet.Cells[row, 7] = ex.Message;
Console.WriteLine($"程序运行出现异常,请关注异常信息。{ex.Message}");
row++;
}
}
var resultFilePath = ConfigurationManager.AppSettings["ResultFilePath"];
if (File.Exists(resultFilePath))
{
File.Delete(resultFilePath);
}
deployWorksheet.Columns[1].AutoFit();//自动列宽
deployWorksheet.Columns[2].AutoFit();//自动列宽
deployWorksheet.Columns[3].AutoFit();//自动列宽
deployWorksheet.Columns[4].AutoFit();//自动列宽
deployWorksheet.Columns[5].AutoFit();//自动列宽
deployWorksheet.Columns[6].AutoFit();//自动列宽
deployWorksheet.Columns[7].AutoFit();//自动列宽
deployWorkbook.SaveAs(Filename: resultFilePath, FileFormat: XlFileFormat.xlWorkbookDefault);
deployWorkbook.Close();
excelApp.Quit();
}
else
{
throw new Exception($"连接Dynamics 365报错.{crmServiceClient.LastCrmError}");
}
}
catch (Exception ex)
{
Console.WriteLine($"程序运行出错:{ex.Message};{ex.StackTrace}");
}
}
else
{
Console.WriteLine("你取消了程序运行!");
}
Console.WriteLine("程序运行结束,按任意键退出!");
Console.ReadKey();
}

static string ChangeColumnType(string typeInXml)
{
string returnVal = typeInXml;
if (typeInXml.Equals("picklist", StringComparison.OrdinalIgnoreCase))
{
returnVal = "PicklistType";
}
else if (typeInXml.Equals("primarykey", StringComparison.OrdinalIgnoreCase))
{
returnVal = "UniqueidentifierType";
}
else if (typeInXml.Equals("nvarchar", StringComparison.OrdinalIgnoreCase))
{
returnVal = "StringType";
}
else if (typeInXml.Equals("ntext", StringComparison.OrdinalIgnoreCase))
{
returnVal = "MemoType";
}
else if (typeInXml.Equals("float", StringComparison.OrdinalIgnoreCase))
{
returnVal = "DoubleType";
}
else if (typeInXml.Equals("int", StringComparison.OrdinalIgnoreCase))
{
returnVal = "IntegerType";
}
else if (typeInXml.Equals("money", StringComparison.OrdinalIgnoreCase))
{
returnVal = "MoneyType";
}
else if (typeInXml.Equals("lookup", StringComparison.OrdinalIgnoreCase))
{
returnVal = "LookupType";
}
else if (typeInXml.Equals("datetime", StringComparison.OrdinalIgnoreCase))
{
returnVal = "DateTimeType";
}
else if (typeInXml.Equals("bit", StringComparison.OrdinalIgnoreCase))
{
returnVal = "BooleanType";
}
else if (typeInXml.Equals("image", StringComparison.OrdinalIgnoreCase))
{
returnVal = "ImageType";
}
else if (typeInXml.Equals("decimal", StringComparison.OrdinalIgnoreCase))
{
returnVal = "DecimalType";
}
else if (typeInXml.Equals("owner", StringComparison.OrdinalIgnoreCase))
{
returnVal = "OwnerType";
}
else if (typeInXml.Equals("state", StringComparison.OrdinalIgnoreCase))
{
returnVal = "StateType";
}
else if (typeInXml.Equals("status", StringComparison.OrdinalIgnoreCase))
{
returnVal = "StatusType";
}
else if (typeInXml.Equals("file", StringComparison.OrdinalIgnoreCase))
{
returnVal = "FileType";
}
else if (typeInXml.Equals("multiselectpicklist", StringComparison.OrdinalIgnoreCase))
{
returnVal = "MultiSelectPicklistType";
}
else if (typeInXml.Equals("uniqueidentifier", StringComparison.OrdinalIgnoreCase))
{
returnVal = "UniqueidentifierType";
}
return returnVal;
}
}

public class TableMetadata
{
public string TableDispalyName { get; set; }

public string TableSchemaName { get; set; }

public List<ColumnMetadata> Columns { get; set; }

public List<FormMetadata> Forms { get; set; }

public List<SavedQueryMetadata> SavedQueries { get; set; }

public List<VisualizationMetadata> Visualizations { get; set; }
}

public class ColumnMetadata
{
public string LogicalName { get; set; }

public string DataType { get; set; }
}

public class FormMetadata
{
public Guid FormId { get; set; }

public string Name { get; set; }
}

public class SavedQueryMetadata
{
public Guid SavedQueryId { get; set; }

public string Name { get; set; }
}

public class VisualizationMetadata
{
public Guid SavedQueryVisualizationId { get; set; }

public string Name { get; set; }
}
}

配置文件实例内容如下:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
</startup>
<appSettings>
<add key="connectStr" value="AuthType=OAuth;Url=https://thomasluotarget.crm5.dynamics.com;UserName=admin@CRM604750.onmicrosoft.com;Password=Q****J;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97;LoginPrompt=Auto" />
<!--<add key="connectStr" value="AuthType=ClientSecret;url=https://xxxx.crm.dynamics.cn/;ClientId=54f2327-274c-2dd0-bde2-ertf571ery6e;ClientSecret=2******Uzxht" />-->
<add key="CustomizationsFilePath" value="C:\Users\yolu\Downloads\Main_1_0_0_13\customizations.xml" />
<add key="ResultFilePath" value="C:\DeployPreCheckResult.xlsx" />
</appSettings>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-11.0.0.0" newVersion="11.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>


运行界面如下,需要输入Y确认后才会运行。

使用程序生成要部署的实体和目标环境实体的差别_元数据


打开生成Excel文件如下:

使用程序生成要部署的实体和目标环境实体的差别_元数据_02

修改内容我分为新增或者修改,修改不一定修改,因为我没有完全去比较是否做了修改,新增是新增。

但是如果字段类型修改了,我还是可以比较出来,因为这个修改会导致部署报错,我特意做了比较。

如果发现了示例如下,组件类型为字段,说明列会有说明类型做什么样的转换。

使用程序生成要部署的实体和目标环境实体的差别_Dynamics 365_03

你问我为啥不找删除的?因为加入的可能是部分子组件,并不会全部加入,生成删除信息可能带来误解,如果需要可以自己加上。

组件类型我分为表、字段、表单、视图、图表几个分类,其他的我未作比较。

欢迎大家使用本程序,并提出好的建议,我会改进程序并公布改进后的程序给大家用。