Dynamics CRM 個人ビューのエクスポートとインポート その 2

みなさん、こんにちは。

前回に続き、開発者向けの情報として、個人ビューのエクスポート
とインポートを考察した結果とサンプルコードを公開します。尚
対象は Microsoft Dynamics CRM 2013 および同等バージョンの
Microsoft Dynamics CRM Online とします。

今回は、前回エクスポートした個人ビューのインポートを考えます。
※以下で紹介する内容はあくまで個人の見解であり、コードは
自己責任で検証を行ってください。

ファイルとユーザーの取得

まずは前回ディスクにシリアライズして保存したデータをデシリア
ライズして読み込むところから検討してみます。まず保存した XML
ファイルを以下のコードで取得しました。

// フォルダ内で UserQuery で始まる XML ファイルを全て取得
string[] files = System.IO.Directory.GetFiles(".", "UserQuery_*xml",
System.IO.SearchOption.AllDirectories);

上記ではプログラムの実行フォルダに XML ファイルが存在する
想定となっています。取得したファイルはユーザー単位となっている
ため、ファイルを 1 つづつ処理します。またファイル処理時に、
新しい環境のユーザー情報を取得する必要があります。

// それぞれのファイルごとに処理
foreach(string file in files)
{
    // ユーザーの特定
    string domainname = file.Substring(file.IndexOf('_') + 1, file.IndexOf(".xml") - file.IndexOf('_') - 1);

   // FetchXML を利用してユーザーを取得
   SystemUser user = (SystemUser)_serviceProxy.RetrieveMultiple(new FetchExpression(
        String.Format(@"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
<entity name='systemuser'>
<attribute name='fullname' />
<filter type='and'>
<condition attribute='domainname' operator='eq' value='{0}' />
</filter>
</entity>
</fetch>", domainname))).Entities.FirstOrDefault();

    // もしユーザーがいない場合は次のファイルを処理
    if (user == null)
        continue;

    // ここで個人ビュー復元処理
}

個人ビューの復元

新しい環境のユーザーが特定できた時点で、データの復元を検討
していきます。

// ファイルから UserQuery をデシリアライズ
DataContractSerializer serializer = new DataContractSerializer(typeof(EntityCollection));

XmlReader reader = XmlReader.Create(file);
//XMLファイルから読み込み、デシリアライズする
EntityCollection views = (EntityCollection)serializer.ReadObject(reader);
//ファイルを閉じる
reader.Close();

// 取得した UserQuery を 1 件ずつ処理
foreach(var view in views.Entities)
{
    // ここで個別の個人ビューを処理
}

これで view ローカル変数にデータを復元できました。次に
個人ビューを 1 件づつ復元していきますが、まずは所有者の
情報を新しいものに変更します。

// 取得したデータを UserQuery 型に変換
UserQuery userQuery = view.ToEntity<UserQuery>();
// 所有者を設定
userQuery.OwnerId = user.ToEntityReference();

次に、ビューがカスタムエンティティのものであった場合は、
前回の考察より LayoutXml の object 属性値が、環境によって
異なる可能性があることが分かっているため、更新を考えます。

カスタムエンティティ用のビューかは以下条件で判定します。

if(userQuery.ReturnedTypeCode.Contains("_"))

新しい環境のカスタムエンティティの ObjectTypeCode を以下の
メタデータクエリで取得します。以下のコードでは情報が取得
できなかった時のことは考えていません。新しい環境にすでに
ソリューションが適用済であると想定しています。

// エンティティフィルターを作成
MetadataFilterExpression EntityFilter = new MetadataFilterExpression(LogicalOperator.And);
// スキーマ名として個人ビューの ReturnedTypeCode を指定
EntityFilter.Conditions.Add(new MetadataConditionExpression("SchemaName", MetadataConditionOperator.Equals, userQuery.ReturnedTypeCode.ToString()));
// ObjectTypeCode だけをクエリ
MetadataPropertiesExpression EntityProperties = new MetadataPropertiesExpression()
{
    AllProperties = false
};
EntityProperties.PropertyNames.Add("ObjectTypeCode");

// クエリの作成
EntityQueryExpression entityQueryExpression = new EntityQueryExpression()
{
    Criteria = EntityFilter
};

// リクエストの作成
RetrieveMetadataChangesRequest retrieveMetadataChangesRequest = new RetrieveMetadataChangesRequest()
{
    Query = entityQueryExpression
};

// エンティティのメタデータを取得
EntityMetadata meta =  ((RetrieveMetadataChangesResponse)_serviceProxy.Execute(retrieveMetadataChangesRequest)).EntityMetadata.First();
int objectTypeCode = (int)meta.ObjectTypeCode;

これで新しい環境におけるカスタムエンティティの ObjectTypeCode が
分かったので、以下コードで LyaoutXml の値を差し替えます。

// LayoutXml の変更
var layoutxml = XDocument.Parse(userQuery.LayoutXml);
layoutxml.Root.Attribute("object").Value = objectTypeCode.ToString();
userQuery.LayoutXml = layoutxml.ToString();

最後に個人ビューを作成します。

_serviceProxy.Create(userQuery);

以下に全コードを示します。

// フォルダ内で UserQuery で始まる XML ファイルを全て取得
string[] files = System.IO.Directory.GetFiles(".", "UserQuery_*xml", System.IO.SearchOption.AllDirectories);
               
// それぞれのファイルごとに処理
foreach(string file in files)
{
    // ユーザーの特定
    string domainname = file.Substring(file.IndexOf('_') + 1, file.IndexOf(".xml") - file.IndexOf('_') - 1);

   // FetchXML を利用してユーザーを取得
   SystemUser user = (SystemUser)_serviceProxy.RetrieveMultiple(new FetchExpression(
        String.Format(@"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
<entity name='systemuser'>
<attribute name='fullname' />
<filter type='and'>
<condition attribute='domainname' operator='eq' value='{0}' />
</filter>
</entity>
</fetch>", domainname))).Entities.FirstOrDefault();
    // もしユーザーがいない場合は次のファイルを処理
    if (user == null)
        continue;

    // ファイルから UserQuery をデシリアライズ
    // DataContractSerializer を前回と同じタイプで生成
    DataContractSerializer serializer = new DataContractSerializer(typeof(EntityCollection));

    // XML リーダーを生成しファイルから読み込み
    XmlReader reader = XmlReader.Create(file);
    //XMLファイルから読み込み、デシリアライズする
    EntityCollection views = (EntityCollection)serializer.ReadObject(reader);
    //ファイルを閉じる
    reader.Close();

    // 取得した UserQuery を 1 件ずつ処理
    foreach(var view in views.Entities)
    {
        // 取得したデータを UserQuery 型に変換
        UserQuery userQuery = view.ToEntity<UserQuery>();
        // 所有者を設定
        userQuery.OwnerId = user.ToEntityReference();
                       

        // カスタムエンティティの場合 (エンティティ名にアンダースコアがある場合)
        if(userQuery.ReturnedTypeCode.Contains("_"))
        {
            // エンティティフィルターを作成
            MetadataFilterExpression EntityFilter = new MetadataFilterExpression(LogicalOperator.And);
            // スキーマ名として個人ビューの ReturnedTypeCode を指定
            EntityFilter.Conditions.Add(new MetadataConditionExpression("SchemaName", MetadataConditionOperator.Equals, userQuery.ReturnedTypeCode.ToString()));
            // ObjectTypeCode だけをクエリ
            MetadataPropertiesExpression EntityProperties = new MetadataPropertiesExpression()
            {
                AllProperties = false
            };
            EntityProperties.PropertyNames.Add("ObjectTypeCode");

            // クエリの作成
            EntityQueryExpression entityQueryExpression = new EntityQueryExpression()
            {
                Criteria = EntityFilter
            };

            // リクエストの作成
            RetrieveMetadataChangesRequest retrieveMetadataChangesRequest = new RetrieveMetadataChangesRequest()
            {
                Query = entityQueryExpression
            };

            // エンティティのメタデータを取得
            EntityMetadata meta =  ((RetrieveMetadataChangesResponse)_serviceProxy.Execute(retrieveMetadataChangesRequest)).EntityMetadata.FirstOrDefault();
            int objectTypeCode = (int)meta.ObjectTypeCode;

            // LayoutXml の変更
            var layoutxml = XDocument.Parse(userQuery.LayoutXml);
            layoutxml.Root.Attribute("object").Value = objectTypeCode.ToString();
            userQuery.LayoutXml = layoutxml.ToString();
                          
        }
        // 個人ビューの作成
        _serviceProxy.Create(userQuery);
    }
}

その他の考察

前回と今回のコード内での懸念点を以下の通りです。

- ユーザーの特定を DomainName で行えるか。たとえばオンライン
環境のようにドメインが変わる場合は他のフィールドで特定が必要。

- 個人ビュー作成時に GUID が指定されている。ベストプラクティス
では Microsoft Dynamics CRM サーバーに Guid の付与は任せるべき
であるためデシリアライズしたものをそのまま利用せず、新しい
インスタンスに各種値だけ渡して処理すべきか。

- 既に同じ名前のビューが存在する場合の重複処理、また各種処理
で失敗した場合のハンドリングを考えていない。等。

まとめ

既定のエクスポート、インポートで移動できないデータは、SDK を
利用して移動するか、手動で再作成する必要があります。その際
数々の考慮が必要であることが今回の記事から読み取れます。

試行錯誤が必要になると思いますが、是非チャレンジしてください!

- 中村 憲一郎