我是微軟Dynamics 365 & Power Platform方面的工程師/顧問羅勇,也是2015年7月到2018年6月連續三年Dynamics CRM/Business Solutions方面的微軟最有價值專家(Microsoft MVP),歡迎關注我的微信公衆号 MSFTDynamics365erLuoYong ,回複466或者20220313可友善擷取本文,同時可以在第一間得到我釋出的最新博文資訊,follow me!
我們釋出解決方案到目标環境,希望釋出的内容盡量精準,釋出過去的元件盡量隻是需要釋出的,不要帶其他不需要釋出的元件,如果要檢查呢?一個個手工比較太Low。我目前沒有找到很好的辦法,根據項目的實踐,一般容易造成問題的主要是實體以及它的元件,我就準備了一個程式,讀取導出解決方案的 customizations.xml 内容和要導入的目标環境的實體中繼資料進行對比,然後将對比結果生成Excel友善分析和比較。
關于查詢實體中繼資料可以參考我前面的博文:
- Dynamics CRM使用中繼資料之一:查詢實體的主字段(托管代碼版本)
- Dynamics CRM 2015/2016新特性之二十七:使用Web API查詢中繼資料
- Dynamics 365使用JavaScript調用Web API批量設定字段的稽核屬性為禁用
程式使用的主要代碼如下:
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;[email protected];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檔案如下:
修改内容我分為新增或者修改,修改不一定修改,因為我沒有完全去比較是否做了修改,新增是新增。
但是如果字段類型修改了,我還是可以比較出來,因為這個修改會導緻部署報錯,我特意做了比較。
如果發現了示例如下,元件類型為字段,說明列會有說明類型做什麼樣的轉換。
你問我為啥不找删除的?因為加入的可能是部分子元件,并不會全部加入,生成删除資訊可能帶來誤解,如果需要可以自己加上。