[Dynamics CRM] カスタムエンティティとプラグインを使用して多対多(Many to Many) 関連をインポートする では、多対多関連をカスタムエンティティとプラグインを使用してインポートできるようにする手順を紹介しました。そこで使用した多対多関連プラグインの実装のメモです。

プログラムの動作確認は Windows Server 2008 上に 英語版の Dynamics CRM 4.0 UR 11 を適用した環境で行っています。

1.多対多関連設定プラグイン

多対多関連設定プラグインのソースは次の通りです。プラグインでは多対多(Many to Many)関連のメタデータを検索して、多対多関連を構成する2つのエンティティ情報を取得します。各エンティティの主属性を調べるために、エンティティのメタ情報を取得しています。その後、np_entity1,np_entity2で設定された値をもとにエンティティレコードの検索を行い、多対多関連を設定しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Metadata;
using System.Web.Services.Protocols;
using System.Xml;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.SdkTypeProxy.Metadata;
using Microsoft.Crm.Sdk.Query;

namespace NetPlanetes.Crm.ManyToManyRelationshipPlugin
{
    /// <summary>
    /// np_manytomanyrelationship にレコードをインポートや画面から作成したときに、
    /// np_relationshipname をN:N関連名, np_entity1 と np_entity2 に関連する
    /// エンティティの主属性とみなしてエンティティレコードを検索し、関連を
    /// 作成するプラグイン。
    /// 
    /// プラグインが失敗する場合はentity1とentity2の値を入れ替えてください。
    /// </summary>
    public class ManyToManyRelationshipPlugin : IPlugin
    {
        #region constant property
        private static readonly string Np_relationshipname = "np_relationshipname";
        private static readonly string Np_entity1 = "np_entity1";
        private static readonly string Np_entity2 = "np_entity2";
        private static readonly string Np_remarks = "np_remarks";
        #endregion

        #region IPlugin Members
        /// <summary>
        /// プラグインの実行のエントリ
        /// </summary>
        /// <param name="context"></param>
        public void Execute(IPluginExecutionContext context)
        {
            try
            {
                ValidateContext(context);           // check context
                ProcessAssociateEntities(context);  // make association
            }
            catch (Exception e)
            {
                if (e is InvalidPluginExecutionException) throw;
                else throw new InvalidPluginExecutionException(e.Message, e);
            }
        }
        #endregion

        /// <summary>
        /// 検証済みプラグインコンテキストを使用して、多対多の設定を行う。
        /// </summary>
        /// <param name="context"></param>
        private void ProcessAssociateEntities(IPluginExecutionContext context)
        {
            DynamicEntity entity = context.InputParameters[ParameterName.Target] as DynamicEntity;
            string relationshipname = entity.Properties[ManyToManyRelationshipPlugin.Np_relationshipname] as string;
            string entity1 = entity.Properties[ManyToManyRelationshipPlugin.Np_entity1] as string;
            string entity2 = entity.Properties[ManyToManyRelationshipPlugin.Np_entity2] as string;
            try
            {
                // Retrieve many to many relationsihp metadata
                IMetadataService metaService = context.CreateMetadataService(true);
                ManyToManyMetadata manyToManyMetadata = GetManyToManyMetadata(metaService, relationshipname);

                // Retrieve entity1 guid and entity2 guid
                ICrmService crmService = context.CreateCrmService(true);
                Guid entity1id = GetEntityId(metaService, crmService, manyToManyMetadata.Entity1LogicalName, entity1);
                Guid entity2id = GetEntityId(metaService, crmService, manyToManyMetadata.Entity2LogicalName, entity2);

                // Associate entities.
                AssociateEntities(crmService, relationshipname, manyToManyMetadata.Entity1LogicalName, entity1id, manyToManyMetadata.Entity2LogicalName, entity2id);
            }
            catch (SoapException se)
            {
                StringBuilder builder = new StringBuilder();
                XmlNode code = se.Detail.SelectSingleNode("//code");
                if (code != null)
                {
                    // ignore duplicate key error and add remark text.
                    if (code.InnerText == "0x80040237")
                    {
                        if (!entity.Properties.Contains(ManyToManyRelationshipPlugin.Np_remarks))
                        {
                            entity.Properties.Add(new StringProperty(ManyToManyRelationshipPlugin.Np_remarks, "ignore duplicate key."));
                        }
                        else
                        {
                            string remark = entity.Properties[ManyToManyRelationshipPlugin.Np_remarks] as string;
                            entity.Properties[ManyToManyRelationshipPlugin.Np_remarks] = remark + "ignore duplicate key.";
                        }
                        return;
                    }
                    else
                    {
                        builder.Append("Error Code:" + code.InnerText);
                    }
                }
                XmlNode description = se.Detail.SelectSingleNode("//description");
                if (description != null)
                {
                    builder.Append("Error Message:" + description.InnerText);
                }
                ThrowException(builder.ToString(), se);
            }
        }
        /// <summary>
        /// 引数で指定された関連名、エンティティ名1、エンティティ1のGUID,エンティティ名2,エンティティ2のGUID
        /// から関連を作成します。
        /// </summary>
        /// <param name="crmService">CRMサービス</param>
        /// <param name="relationshipname">N:N関連名</param>
        /// <param name="entity1LogicalName">エンティティ1の論理名</param>
        /// <param name="entity1id">エンティティ1のレコードのGUID</param>
        /// <param name="entity2LogicalName">エンティティ2の論理名</param>
        /// <param name="entity2id">エンティティ2のレコードのGUID</param>
        private void AssociateEntities(ICrmService crmService, string relationshipname, string entity1LogicalName, Guid entity1id, string entity2LogicalName, Guid entity2id)
        {
            Moniker moniker1 = new Moniker(entity1LogicalName, entity1id);
            Moniker moniker2 = new Moniker(entity2LogicalName, entity2id);

            AssociateEntitiesRequest request = new AssociateEntitiesRequest();
            request.Moniker1 = moniker1;
            request.Moniker2 = moniker2;
            request.RelationshipName = relationshipname;

            crmService.Execute(request);
        }
        /// <summary>
        /// 多対多関連のメタデータを検索します。
        /// </summary>
        /// <param name="metaService">メタデータサービス</param>
        /// <param name="relationshipName">検索する関連名</param>
        /// <returns>ManyToManyMetadata</returns>
        private ManyToManyMetadata GetManyToManyMetadata(IMetadataService metaService, string relationshipName)
        {
            RetrieveRelationshipRequest request = new RetrieveRelationshipRequest();
            request.Name = relationshipName;
            // retrieve currently published relationship
            request.RetrieveAsIfPublished = false;
            RetrieveRelationshipResponse response = metaService.Execute(request) as RetrieveRelationshipResponse;

            ManyToManyMetadata metadata = response.RelationshipMetadata as ManyToManyMetadata;
            if (metadata == null)
            {
                ThrowException("relationshipname is not many to many relationsihpname");
            }
            return metadata;
        }
        /// <summary>
        /// 引数で指定されたentityLocialName のEntityMetadata を取得します。
        /// EntityMetadataにはEntityの情報のみが設定されます。
        /// </summary>
        /// <param name="metadataService">メタデータサービス</param>
        /// <param name="entityLogicalName">検索するエンティティの論理名</param>
        /// <returns>EntityMetadata</returns>
        private EntityMetadata GetEntityMetadata(IMetadataService metadataService, string entityLogicalName)
        {
            // Retrieve Primary Field
            RetrieveEntityRequest request = new RetrieveEntityRequest();
            request.LogicalName = entityLogicalName;
            request.RetrieveAsIfPublished = false;
            request.EntityItems = EntityItems.EntityOnly;

            RetrieveEntityResponse response = metadataService.Execute(request) as RetrieveEntityResponse;

            return response.EntityMetadata;

        }
        /// <summary>
        /// 引数で指定されたエンティティ名のエンティティのなかで、primaryFieldValue と主属性が一致する
        /// エンティティのレコードを検索し、GUIDを取得します。
        /// </summary>
        /// <param name="metadataService">メタデータサービス</param>
        /// <param name="crmService">Crmサービス</param>
        /// <param name="entityLogicalName">検索するエンティティの名前</param>
        /// <param name="primaryFieldValue">フィルタ条件に使用する主属性の値</param>
        /// <returns></returns>
        private Guid GetEntityId(IMetadataService metadataService, ICrmService crmService, string entityLogicalName, string primaryFieldValue)
        {
            EntityMetadata entityMetadata = GetEntityMetadata(metadataService, entityLogicalName);
            string primaryField = entityMetadata.PrimaryField;
            string primaryKey = entityMetadata.PrimaryKey;

            // Retrieve Entity Record Id
            QueryExpression query = new QueryExpression(entityLogicalName);
            query.ColumnSet = new ColumnSet();
            ConditionExpression condition = new ConditionExpression(primaryField, ConditionOperator.Equal, primaryFieldValue);
            query.Criteria.AddCondition(condition);

            RetrieveMultipleRequest request = new RetrieveMultipleRequest();
            request.ReturnDynamicEntities = true;
            request.Query = query;
            RetrieveMultipleResponse retrieveResponse = crmService.Execute(request) as RetrieveMultipleResponse;
            BusinessEntityCollection entities = retrieveResponse.BusinessEntityCollection;
            if (entities.BusinessEntities.Count != 1)
            {
                ThrowException("0 or many entity record is found.");
            }
            return ((entities.BusinessEntities[0] as DynamicEntity).Properties[primaryKey] as Key).Value;
        }
        /// <summary>
        /// プラグインが起動されたときにメッセージとエンティティのチェック
        /// および、必要な属性が指定されているかを判定する。
        /// </summary>
        /// <param name="context"></param>
        private void ValidateContext(IPluginExecutionContext context)
        {
            // check message and entity
            if (context.MessageName != MessageName.Create)
            {
                ThrowException("unsupported message");
            }
            if (!context.PrimaryEntityName.Equals("np_manytomanyrelationship"))
            {
                ThrowException("unsupported entity");
            }
            // check if attribute is exists
            DynamicEntity entity = context.InputParameters[ParameterName.Target] as DynamicEntity;
            if (!entity.Properties.Contains(ManyToManyRelationshipPlugin.Np_relationshipname))
            {
                ThrowException("relationshipname is required");
            }
            if (!entity.Properties.Contains(ManyToManyRelationshipPlugin.Np_entity1))
            {
                ThrowException("entity1 is required ");
            }
            if (!entity.Properties.Contains(ManyToManyRelationshipPlugin.Np_entity2))
            {
                ThrowException("entity2 is required ");
            }
        }
        #region helper method
        /// <summary>
        /// 例外を発生させるヘルパーメソッド
        /// </summary>
        /// <param name="message">エラーメッセージ</param>
        /// <param name="innerException">オリジナルの例外</param>
        private void ThrowException(string message, Exception innerException)
        {
            throw new InvalidPluginExecutionException(message, innerException);
        }
        /// <summary>
        /// 例外を発生させるヘルパーメソッド
        /// </summary>
        /// <param name="message">エラーメッセージ</param>
        private void ThrowException(string message)
        {
            ThrowException(message, null);
        }
        #endregion

    }
}

プログラムではエラーメッセージを例外でスロー措定ます。エンティティフォーム上でレコードを作成したときにはエラーメッセージの内容がエラーダイアログ上に表示されます。残念ながら、標準インポート時にデータにエラーがありプラグインで例外がスローされた場合、機械的なエラーメッセージがインポートの失敗レコードの列に表示されるので注意してください。

2.まとめ

説明はあんまりしていませんが、長くはないので理解はできると思います。プログラムを見てわかるとおり、5回位 MetadataService と CrmSer vice を呼び出しています。関連やエンティティのメタ情報は一定時間キャッシュするようにすればパフォーマンスを向上させることができると思います。