Dynamics CRM 2011 の組織DBに作成される フィルター付きのビュー (FilteredSystemUser等) にクエリを発行するために EF 4.1から使用できるようになった Code First を使用してクエリを発行するメモです。EF 4 Code First はプログラムからFilteredView にアクセスするテクノロジとしてはすばらしく適合していると思います。

動作確認環境

  • EFのバージョン:Entity Framework 4.3
  • 組織DBのSQL Server: SQL Server 2008 R2 Developer Edition

本サンプルは、プログラムを実行するユーザは Dynamics CRM 2011 にアクセスする権限のあるユーザであることを前提としています。

今回は、ユーザ、部署、ロールのFilteredView に対してクエリを発行するサンプルを作成してみます。サンプルではEF4Code First に関して細かい説明はしていません。必要に応じて、Entity Framework 4 Code First メモ や、リンク先の紹介されているページを参照してください。

1.サンプルプログラムの作成

1.1 プロジェクトの準備

Visual Studio 2010を起動して、 コンソールプロジェクトを作成します。本例では、 CrmEF4CF01 という名前で作成しました。プロジェクトを右クリックして Manage NuGet Package を選択します。Manage NuGet Package 画面が表示されるので、 Onlineタブを選択し、 EntityFramework を検索してインストールします。NuGetのインストールは[Visual Studio 2010]NuGet Package Manager をインストールする 、EntityFramework のインストールについては[EntityFramework4] はじめての EntityFramework 4 CodeFirstが参考になります。

 

EntityFramework は NuGet Package Manager Console からもインストールできます。Visual Studio の ツールメニュー→Library Package Manager → Package Manager Console を選択して、表示されたPackage Manager Console で、次のコマンドを入力します。

PM> Install-Package EntityFramework

以上で準備完了です。

1.2 コードファーストに必要なクラスの作成

プロジェクトに Model.cs クラスを作成します。次のように編集します。今回は、ユーザ、部署、ロールエンティティに対してクエリを発行するサンプルを作成するので、使用する属性のみを定義しています。N:N関係に関しては FluentAPI を使用しています。TableAttribute を使用して、各モデルクラスとエンティティ用のビューへ のマッピングを設定しています。属性名はエンティティのフィールドの物理名と同じにしています。そうでない場合はColumnAttribute を使用してマッピングを設定する必要があります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;

namespace CrmEF4CF01
{
    /// <summary>
    /// systemuserを表すドメインクラス
    /// </summary>
    [Table("FilteredSystemUser")]
    public class SystemUser
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid SystemUserId { get; set; }

        public string Fullname { get; set; }
        public string Domainname { get; set; }
        public bool? IsDisabled { get; set; }
        [ForeignKey("BusinessUnit")]
        public Guid? BusinessUnitId { get; set; }
        /// <summary>
        /// 所属する部署。
        /// systemuser は businessunit に対して複数のリレーション
        /// があるので、InverseProperty で参照先を指定しておく。
        /// </summary>
        [InverseProperty("Members")]
        public virtual BusinessUnit BusinessUnit { get; set; }
        public virtual ICollection<Role> Roles { get; set; }
    }
    /// <summary>
    /// 部署を表すドメインクラス
    /// </summary>
    [Table("FilteredBusinessUnit")]
    public class BusinessUnit
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid BusinessUnitId { get; set; }
        public string Name { get; set; }
        public Guid? ParentBusinessUnitId { get; set; }
        [ForeignKey("ParentBusinessUnitId")]
        public virtual BusinessUnit ParentBusinessUnit { get; set; }
        public virtual ICollection<SystemUser> Members { get; set; }
    }

    [Table("FilteredRole")]
    public class Role
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid RoleId { get; set; }
        public string Name { get; set; }
        public ICollection<SystemUser> RoleMembers { get; set; }
    }
    public class DynMSCRMContext : DbContext
    {
        public DynMSCRMContext(string nameOrConnectionString) : base(nameOrConnectionString) { }

        public DbSet<SystemUser> SysteUsers { get; set; }
        public DbSet<BusinessUnit> BusinessUnits { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // N:N関連はDataAnnotationsで表現できないので FluentAPI を使用して設定
            modelBuilder.Entity<SystemUser>().HasMany(s => s.Roles)
                .WithMany(r => r.RoleMembers)
                .Map(x =>
                {
                    x.ToTable("FilteredSystemUserRoles").MapLeftKey("systemuserid").MapRightKey("roleid");
                });
            
        }
    }
}

モデルクラスとコンテキストクラスの準備が整いました。既定で作成されるProgram.cs を次のように編集してクエリを発行するプログラムを作成します。ナビゲーションプロパティを設定知りているため、非常にきれいに関連するエンティティの情報にアクセスできることがわかります。もちろん遅延ローディングも行えます。Entity Framework が発行するクエリも後で紹介するデータベースファーストを使用した例と比べてシンプルになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity.Infrastructure;
using System.Data.Entity;

namespace CrmEF4CF01
{
    class Program
    {
        static void Main(string[] args)
        {

            // ユーザ情報と所属する部署を表示する(1:NでN側から1の情報をロード)
            using (DynMSCRMContext context = new DynMSCRMContext("name=crm"))
            {
                foreach (var user in context.SysteUsers.Where(x => x.IsDisabled == false))
                {
                    Console.WriteLine("ユーザ名:{0}", user.Fullname);
                    Console.WriteLine("\t部署 :{0}", user.BusinessUnit.Name);
                }
            }

            // 部署に所属するユーザを表示する(1:Nで1側からNの情報をロード
            using (DynMSCRMContext context = new DynMSCRMContext("name=crm"))
            {
                foreach (var bu in context.BusinessUnits.AsEnumerable())
                {
                    Console.WriteLine("部署名:{0}", bu.Name);
                    foreach (var user in bu.Members)
                    {
                        Console.WriteLine("\t{0}", user.Fullname);
                    }
                }
            }
            // ユーザごとに所属ロールの列挙(N:N関連の確認)
            using (DynMSCRMContext context = new DynMSCRMContext("name=crm"))
            {
                foreach (var user in context.SysteUsers.Where(x=>x.IsDisabled == false))
                {
                    Console.WriteLine("ユーザ:{0}", user.Fullname);
                    foreach (var role in user.Roles)
                    {
                        Console.WriteLine("\tロール{0}", role.Name);
                    }
                }
            }
            // ユーザごとに所属ロールの列挙 Eager Load
            using (DynMSCRMContext context = new DynMSCRMContext("name=crm"))
            {
                foreach (var user in context.SysteUsers.Include(x=>x.Roles).Where(x => x.IsDisabled == false))
                {
                    Console.WriteLine("ユーザ:{0}", user.Fullname);
                    foreach (var role in user.Roles)
                    {
                        Console.WriteLine("\tロール{0}", role.Name);
                    }
                }
            }
            Console.ReadLine();
        }
    }
}

プログラムの作成が終わったら、構成ファイルを編集しましょう。 

1.3 構成ファイルの設定

構成ファイルに次の設定を行います。

  • プログラム内で指定している接続文字列
  • Entity Framework 4 Code First のDbContextクラスが最初にアクセスされたときに行うデータベースの初期化処理の無効化

データベースの初期化を無効にするのは、CRMの場合Dynamics CRM 2011のインフラストラクチャが自動でViewを作成するため、行っても意味がないためです。また誤ってデータベースを削除したり、作成したりしないためです。データベースの初期化処理を無効化したので、開発者がDBのビューとモデルのマッピングの責任を持つ必要があります。

設定サンプルを記載します。組織DB名は dyn_MSCRM になっています。環境に合わせて変更してください。また、ループ処理中で遅延ロードを行っているため、multipleactiveresultsets=true を設定してマルチアクティブ結果セットを有効にしています。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
	<configSections>
		<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
		<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.3.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
	</configSections>
	<connectionStrings>
		<add name="crm" connectionString="Server=(local);Initial Catalog=dyn_MSCRM;Integrated Security=true;multipleactiveresultsets=true" providerName="System.Data.SqlClient"/>
	</connectionStrings>
	<entityFramework>
		<contexts>
			<context type="CrmEF4CF01.DynMSCRMContext, CrmEF4CF01" disableDatabaseInitialization="true" />
		</contexts>
	</entityFramework>
</configuration>

EF4.1 コードファーストの細かい内容などはEntity Framework 4 Code First メモ も参考にしてください。

以上で準備完了です。プログラムを実行すると、部署に所属するユーザの一覧や、ユーザの持つロールの一覧などが出力されます。

2. まとめ

説明は以上です。コードファーストを使用すると、プログラムで使用する属性情報とナビゲーションのみを作成しておけば、CRMのエンティティのビューに対するアクセスが非常に簡潔かつ柔軟に行えるようになります。

3. データベースファーストから作成したEntity Data Model を使用する場合の注意点

個人的にはコードファーストが最適だと思いますが、EntityFramework.dll をインストールする必要があるため、Entity Framework 4.0 から使用できる データベースファーストな Entity Framework を使用する必要がある場合もあると思います。その場合の留意事項を記載しておきます。

Visual Studio から FilteredVIew に対して Entity Data Model を作成できることは確認済みです。ただし、各ビューにはナビゲーションプロパティが作成されません。単体のビューとなってしまいます。ビューに主キーや外部キーの情報がないので、当然の結果です。そのため、関連するエンティティと結合する必要がある場合、JOINを駆使する必要があり、LINQ式やEntitySQLが非常に汚くなります。出力されるクエリも複雑かつ非効率になっていきます。ビュー単体にクエリを発行する場合は問題ないですが、各エンティティを結合する場合、JOINを行う必要が多くあると思います。

参考までに Visual Studio の edmx 作成 ウィザードから作成した FilteredView の Entity Data Modelを使用してユーザが保有しているロールの一覧を取得するコードの一部を記載します。N:N関連ということもありますが、joinがめんどくさいです。ナビゲーションプロパティが使用できないので非常にややこしいです。実際にはデザイナ上でナビゲーションを設定できると思いますが、ウィザードを使用すると使用しない属性情報までエンティティモデルとして作成されるのでデザイナ画面がみずらくなりまた、Entity Data Model をメモリ上にロードするときに要する時間が大きくなります。あまり気にすることないかもしれませんが。

using (dyn_MSCRMEntities context = new dyn_MSCRMEntities())
{
      var userroles = from s in context.FilteredSystemUser
                      join sr in context.FilteredSystemUserRoles
                        on s.systemuserid equals sr.systemuserid into sr1
                       from sr11 in sr1.DefaultIfEmpty()
                      join r in context.FilteredRole
                      on sr11.roleid equals r.roleid
                      select new { Name = s.domainname, Roles = r.name } into a
                      group a by a.Name;

      foreach (var i in userroles)
      {
          Console.WriteLine(i.Key);
          foreach (var j in i)
          {
              Console.WriteLine("\t" + 
                  j.Roles);
          }
      }
}