Microsoft Dynamics CRM用のLinq対応クライアント(sdkからのクライアントの代替)

プロジェクトの1つを開発するとき、MS CRMとの統合が必要でした... msdnの標準クエリメカニズムを見ると、それが少し不便であることがわかりました。プロジェクトは長くて無限(内部自動化)になると約束されたため、純粋な形では、人件費の大幅な増加につながり、不注意なミスの繁殖地になります(プロジェクトの開発者は頻繁に変わるため、時間を割く人は誰でも仕事をするため)。



そのため、QueryExpressionのラッパーを作成し、EFのように流なクエリを作成する機能を追加することにしました。 すぐにこのラッパーを作成するときに(中間のどこかに)このような機会を提供するsdkからライブラリを見つけたことを予約します。 :どこでinを使用し、結合する条件を追加し、いくつかの小さな条件を追加します。 後で比較表を示します。



プロジェクトは長くなることが約束されているので、実装を完了することにしました...







タスク:





最終的に起こったこと:



このプロジェクトはアセンブリであり、追加の依存関係は1つだけです。microsoft.xrm.sdk.dllは、プロジェクトに簡単に接続できます。



お客様


アセンブリは、クライアントを作成するための抽象基本クラス-CrmClientBaseを提供します。 このクラスには、オーバーライドする必要がある1つの抽象フィールドがあります。



protected abstract IWcfCrmClient WcfClient { get; }
      
      





IWcfCrmClientは、WCFプロジェクトに追加されたサービス参照クライアントとの通信インターフェイスです。



サンプルを使用してクライアントクラスを作成する方法を示す方がよいです(ほとんどの場合、プロジェクトにコピーして、正しい使用方法とすべてが機能するはずです)。

 using System; using CrmClient; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; using MsCrmClientTest.MSCRM; public class OrgCrmClient : CrmClientBase { private class WcfCrmClient : IWcfCrmClient { private OrganizationServiceClient _client; public Guid Create(Entity entity) { return _client.Create(entity); } public void Update(Entity entity) { _client.Update(entity); } public void Delete(string entityName, Guid id) { _client.Delete(entityName, id); } public EntityCollection RetrieveMultiple(QueryBase query) { return _client.RetrieveMultiple(query); } public OrganizationResponse Execute(OrganizationRequest request) { return _client.Execute(request); } public void Close() { _client.Close(); } public WcfCrmClient() { _client = new OrganizationServiceClient(); } } private IWcfCrmClient _wcfClient; protected override IWcfCrmClient WcfClient { get { if (_wcfClient == null) _wcfClient = new WcfCrmClient(); return _wcfClient; } } }
      
      





OrganizationServiceClientは、Service Referenceのクライアントです



マッピング


CRMエンティティを操作するには、それらをクラスにマップする必要があります(データコントラクトを定義します)。 これには2つの属性があります(microsoft.xrm.sdk.dllアセンブリの標準属性)



属性が指定されていない場合、クラス名/プロパティ名がCRMのエンティティ/フィールド名として使用されます。



各データコントラクトは、基本クラスCrmDataContractBaseから継承する必要があります。 これは、1つの抽象プロパティを持つ抽象クラスです。

 public abstract Guid Id { get; set; }
      
      





オーバーライドし、AttributeLogicalName属性でマークする必要があります。

データコントラクトの例:

 [EntityLogicalName("systemuser")] public class User : CrmDataContractBase { [AttributeLogicalName("systemuserid")] public override Guid Id { get; set; } [AttributeLogicalName("fullname")] public string Name { get; set; } [AttributeLogicalName("parentsystemuserid")] public EntityReference hief { get; set; } [AttributeLogicalName("caltype")] public OptionSetValue CALType }
      
      





重要!



CRM列挙のマッピング


CRM列挙をマップするには、クラスを定義し、CrmOptionsSetBaseから継承し、CRMで列挙の名前を指定するEntityLogicalName属性でマークする必要があります。

 [EntityLogicalName("connectionrole_category")] public class ConnectionRoleCategoryEnum : CrmOptionsSetBase { }
      
      





CrmOptionsSetBaseは、CrmOption型のIEnumerableインターフェイスを実装します。 コントロールのデータソースとしてすぐに使用できます。

CrmOptionクラスには2つのプロパティが含まれています。

 public string Label { get; private set; } public OptionSetValue Value { get; private set; }
      
      





ラベルには要素の表示名が含まれ、値はCRMエンティティのデータコントラクトで使用されるOptionSetValueです



顧客の使用



CRMエンティティの追加、変更、削除


これらは単純な操作であり、例からすべてが明確になっているはずです。

 [EntityLogicalName("new_nsi")] public class NSI : ICrmDataContract { [AttributeLogicalName("new_nsiid")] public override Guid Id { get; set; } [AttributeLogicalName("new_name")] public string Name { get; set; } } // var newnsi = new NSI { Name = "Test NSI" }; _client.Add(newnsi); // 'Add'   'Id'   ,   EF // newnsi.Name = "Test NSI 2"; _client.Update(newnsi); // _client.Delete(newnsi);
      
      





重要! すべての変更はすぐにCRMに適用されます。 取引はありません(まだわかりません)



CRM列挙の取得


クライアントには転送を受信するための特別な方法があります

 public T OptionsSet<T>()
      
      





ここで、Tは転送のデータ契約です。 例(上記のデータの接触):

 var optionSet = _client.OptionsSet<ConnectionRoleCategoryEnum>();
      
      







Linqを使用したCRMのクエリ



ネームスペースCrmClient.Linqは、流fluentなCRM要求を生成するための以下の拡張メソッドを定義します。



この名前空間のメソッドはCrmで始まるため、CRMの要求がどこで形成され、既にアンロードされたオブジェクトの処理がどこで行われているかをすぐに確認できます。

CRMへのリクエストを生成するための開始方法はクエリです:



 public ICrmQueryable<T> Query<T>()
      
      





その後、クエリ生成の他の方法を使用できます。

CRM要求自体が実行されています。 GetEnumerator()メソッドを呼び出すとき、つまり(EFのように)データを列挙しようとするとき



選択してください


匿名タイプ

 var users = _client.Query<CrmUser>() .CrmSelect(u => new { u.Id, u.Name, Test = 1 }) .ToList();
      
      





クラス(EFと同様、クラスにはパラメーターのないコンストラクターが必要です)

 var users2 = _client.Query<CrmUser>() .CrmSelect(u => new TestUser() { Id = u.Id, FullName = u.Name, Test = 1 }) .ToList();
      
      





どこで


 // var user = _client.Query<CrmUser>() .CrmWhere(i => i.Id == _directorUserId) .Single(); var list = new[] { _directorUserId }; // in var filteredUsers = _client.Query<CrmUser>() .CrmWhere(i => list.Contains(i.Id)) .ToList(); // not in filteredUsers = _client.Query<CrmUser>() .CrmWhere(i => !list.Contains(i.Id)) .ToList(); //like var users = _client.Query<CrmUser>().ToList(); var firstUser = users.First(i => i.Name.Contains(" ")).Name.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); // like text% var user = _client.Query<CrmUser>() .CrmWhere(i => i.Name.StartsWith(firstUser[0])) .ToList(); // like %text user = _client.Query<CrmUser>() .CrmWhere(i => i.Name.EndsWith(firstUser[0])) .ToList(); // like %text% user = _client.Query<CrmUser>() .CrmWhere(i => i.Name.Contains(firstUser[0])) .ToList(); // not like text% user = _client.Query<CrmUser>() .CrmWhere(i => !i.Name.StartsWith(firstUser[0])) .ToList(); // not like %text user = _client.Query<CrmUser>() .CrmWhere(i => !i.Name.EndsWith(firstUser[0])) .ToList(); // not like %text% user = _client.Query<CrmUser>() .CrmWhere(i => !i.Name.Contains(firstUser[0])) .ToList();
      
      





重要! 'in'構造の場合、配列のみを渡す必要があります。 この制限は少し後で削除します。



複合条件のサポートもあります( Entity Frameworkの複合キーの EF "WHERE"条件の拡張機能のように)。

 var users = _client.Query<CrmUser>().ToList(); var directors = users.Where(u => u.Director != null).Select(u => new { u.Director.Id, u.Director.Name }).Take(2); var users2 = _client.Query<CrmUser>() .CrmWhere(ExpressionType.Or, directors, (u, d) => u.Id == d.Id && u.Name == d.Name, (pn, o) => { switch (pn) { case "Id": return o.Id; case "Name": return o.Name; default: return null; } }) .ToList();
      
      





ご注文


 var users = _client.Query<CrmUser>() .CrmOrderBy(i => i.Name) .CrmOrderByDescending(i => i.Id) .ToList();
      
      





結果はName ascでソートされ、次にId descでソートされます



明確な


 var users = _client.Query<CrmUser>() .CrmDistinct() .ToList();
      
      





参加する


C参加は少し複雑です...たとえば、ユーザーをリーダーに参加させる必要があります。



 var users = _client.Query<CrmUser>() .CrmJoin(_client.Query<CrmUser>(), s => s.Chief.Id, d => d.Id, (s, d) => new { s.Id, s.Name, ChiefId = d.Id, ChiefFullName = d.Name }) .ToList(); or var users = _client.Query<CrmUser>() .CrmLeftJoin(_client.Query<CrmUser>(), s => s.Chief.Id, d => d.Id, (s, d) => new { s.Id, s.Name, ChiefId = d.Id, ChiefFullName = d.Name }) .ToList();
      
      





要求は正しく実行されます。 ただし、よく見ると、s.ChiefのIdプロパティはEntityReferenceクラスのプロパティであるため、どこにもマップされません...しかし、s.Chiefプロパティ自体は、必要な「parentsystemuserid」CRMにマップされます... 's.Chiefから、表記s => s.Chief.Id、d => d.Idは型の互換性のために必要です。



別の問題は、結合要求の条件の指定です。 CRMリクエストでは、この条件はLinkクラス自体で指定されるため、この条件を示すには、結合リクエスト自体に登録する必要があります。



 var users = _client.Query<CrmUser>() .CrmJoin(_client.Query<CrmUser>().CrmWhere(u => u.Id == _directorUserId), s => s.Chief.Id, d => d.Id, (s, d) => new { s.Id, s.Name, ChiefId = d.Id, ChiefFullName = d.Name }) .ToList(); or var users = _client.Query<CrmUser>() .CrmLeftJoin(_client.Query<CrmUser>().CrmWhere(u => u.Id == _directorUserId), s => s.Chief.Id, d => d.Id, (s, d) => new { s.Id, s.Name, ChiefId = d.Id, ChiefFullName = d.Name }) .ToList();
      
      





_client.Query()。CrmWhere(u => u.Id == _directorUserId)-これは参加の条件です。 つまり これが内部結合の場合、ID _directorUserIdを持つディレクターを持つユーザーのみが返されます。 ここでは、注文などの他の条件を指定できますが、効果はなく、Where条件のみが考慮されます。



ノーロック


このメソッドは、CRM側のリクエストがwith(nolock)オプションで実行されるために必要です。これは、過去の期間のレポートのデータを取得するのに役立ちます。 例:



 var users = _client.Query<CrmUser>() .CrmNoLock() .ToList();
      
      





ページング


CRMクエリは、結果のページ分割をサポートします。 ページリクエストを実行する方法があります



 List<T> CrmGetPage(int pageNumber, int pageSize, out int totalCount, out bool moreRecordsExists)
      
      







Listをすぐに返します。つまり、呼び出しはCRMへの要求をすぐに実行します。

このメソッドは、レコードの総数と、CRM側で受信するデータがまだあるというサインを返します。 例:



 int total; bool moreExists; var users = _client.Query<CrmUser>().CrmGetPage(1, 10, out total, out moreExists);
      
      







(すべての例はテストプロジェクトにあります)



お客様の特徴



クライアントは、最初にマッピングデータを取得するためだけにリフレクションを使用し、このデータはメモリにキャッシュされます。 このため、最初のリクエストは遅くなります...

クラスインスタンスを作成するコードは動的にコンパイルされるため、結果を取得するのにかかる時間は、CRM側でのリクエストの実行時間とネットワーク遅延のみに依存します。 型作成コードはメモリでコンパイルされます-型ごとに1つのアセンブリ。 ただし、1つの注意点があります。匿名型作成は、リフレクションを介してコンストラクターを介して行われます。 これは、異なるアセンブリでは匿名型が異なる内部型を持ち、キャストが不可能であるという事実によるものです。 誰かがこの制限を克服する方法を知っているなら、書いてください、私はそれを修正します。



SDKのクライアントとの比較



速度では、このクライアントとSDKのクライアントはほとんど同じです(テスト中のネットワーク遅延の範囲内です)。 テストCRMのデータ量が少ないため、SDKのクライアントがCRM側ですべてを実行するのか、アプリケーション側で既に何かを実行するのか(たとえば、並べ替えなど)をまだ確認できませんでした。標準のIQueryableおよび標準のLinq拡張メソッド。



比較テスト自体はソースにあります。 実装の結果は次のとおりです。



 Operation ThisCrmClient SdkCrmClient Query 00:00:25.6005598 00:01:01.1291123 Select 00:00:03.5173517 00:00:03.6273627 Order 00:00:08.2558255 00:00:08.2338233 Where 00:00:04.1074107 00:00:03.9203920 WhereIn 00:00:05.3745374 not supported %Like% 00:00:03.3983398 00:00:03.4093409 %Like 00:00:03.4403440 00:00:03.4163416 Like% 00:00:03.3093309 00:00:03.3033303 Join 00:00:03.4313431 00:00:03.4143414 JoinFilter 00:00:03.3833383 not supported NoLock 00:00:09.6899689 not supported Distinct 00:00:07.9847984 00:00:08.0328032
      
      







ビルドとソース



アセンブリをダウンロードして、ソースcodecodex- mscrmclientを確認できます。 ソリューションは3つのプロジェクトで構成されます。



結論の代わりに



codeplexで英語のドキュメントを作成しましたが、ロシア語版のドキュメントはこの記事です。



このクライアントを使用する過程で発見するすべてのエラーは、すぐに修正してcodeplexにアップロードします。 プロジェクトでこのクライアントを使用してエラーを見つけた場合は、プロジェクトページで問題を作成します。 サインアップして通知を受け取りました。 私はできるだけ早くすべてのエラーを修正しようとします(これは、私のプロジェクトでもこのクライアントを使用しているという理由だけで、私の利益のためです)。



All Articles