Entity Framework 4 Code First を使用して CRMの組織 DB にクエリを発行する例を[CRM2011] FilteredView に Entity Framework 4 Code First を使用してクエリを発行する で記載しました。サンプル触れませんでしたが、 構成ファイルを使用できなかったり、別のユーザに偽装して FilteredView にアクセスする必要があるケースがあります。プラグインやカスタムページから FilteredView にクエリを発行するケースがこれに該当します。

FilteredViewにアクセスするときにCRMのユーザに偽装する場合は CONTEXT_INFO に偽装するユーザのGUIDを設定する必要があります。今回はコンソールプログラムから偽装を行えるかを試しました。またプラグインで使う可能性があるので、構成ファイルを一切使用せず EntityFramework 4 Code First を使用してクエリを発行しています。

動作確認環境

  • Entity Framework 4 の Version : 4.3
  • 組織DBにあるSQL Server : SQL Server 2008 R2

前提として、[CRM2011] FilteredView に Entity Framework 4 Code First を使用してクエリを発行する に従って、プロジェクトとクラスは作成済みとします。ただし、プロジェクト名は CrmEF4CF02 としています。プロジェクトの準備などは先のリンクを参照してください。

1. 偽装に必要なクラスを作成する

プロジェクトに新規クラス CrmDbImpersonator.cs を追加します。本クラスでは、 DbContext が接続を Open したときに、偽装を行うクエリを発行します。DbContext のコンストラクタにコネクションがOpenした状態のDbConnection を渡せれば楽なのですが、接続がOpenした状態で DbContextのコンストラクタに DbConnection を設定すると例外が発生します。そのため、接続がOpenしたタイミングで偽装のクエリを発行するようにCrmDbImpersonator クラスを作成しました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;

namespace CrmEF4CF02
{
    class CrmDbImpersonator : IDisposable
    {
        /// <summary>connection</summary>
        private DbConnection Connection { get; set; }
        /// <summary>systemuserid to impersonate</summary>
        private Guid ImpersonationUserId { get; set; }
        /// <summary>base sql to set context info</summary>
        private static readonly string _context_sql = @"
                        DECLARE @__userguid uniqueidentifier
                        SET @__userguid = '{0}'
                        SET CONTEXT_INFO @__userguid
                        ";

        public CrmDbImpersonator(DbConnection connection, Guid impersonationuserid)
        {
            if (connection == null) throw new ArgumentNullException("connection");

            this.Connection = connection;
            this.ImpersonationUserId = impersonationuserid;

            Initialize();
        }
        protected virtual void Initialize()
        {
            this.Connection.StateChange += ConnectionStateChangeCallback;
        }
        public virtual void Dispose()
        {
            this.Connection.StateChange -= ConnectionStateChangeCallback;
        }
        void IDisposable.Dispose()
        {
            this.Dispose();
        }
        void ConnectionStateChangeCallback(object sender, System.Data.StateChangeEventArgs e)
        {
            if (e.CurrentState == System.Data.ConnectionState.Open)
            {
                // 接続の状態が Closed から Open になったら、偽装用のクエリを発行
                using (DbCommand cmd = this.Connection.CreateCommand())
                {
                    cmd.CommandText = string.Format(_context_sql, ImpersonationUserId);
                    cmd.CommandType = System.Data.CommandType.Text;
                    cmd.ExecuteNonQuery();
                }
            }
        }
    }
}

次に、Code First では、既定のDatabase Initializerに、CreateDatabaseIfNotExists が設定されているので、DBがないときにデータベースを作成しない用カスタムのデータベースイニシャライザを実装します。新規クラスを作成します。プロジェクトに CheckDatabaseExistenceOnly.cs を追加して次のようにコードを記述します。カスタムデータベースイニシャライザでは、DBの存在チェックのみを行うようにしています。

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

namespace CrmEF4CF02
{
    /// <summary>
    /// データベースの存在チェックのみを行う。
    /// </summary>
    /// <typeparam name="TContext"></typeparam>
    class CheckDatabaseExistenceOnly<TContext>
        : IDatabaseInitializer<TContext> where TContext : DbContext
    {
        public void InitializeDatabase(TContext context)
        {
            if (context.Database.Exists())
            {
                return;
            }
            throw new ApplicationException(string.Format("Database {0} does not exist.", context.Database.Connection.Database));
        }

        // データベースを作成することがないので Seed メソッドは実装しない
        //protected virtual void Seed(TContext context)
        //{ }
    }
}

インフラストラクチャとして使用するクラスの準備は完了です。

2. Program.cs を変更する 

Program.cs を次のように変更します。プログラムでは プログラム内で設定している接続文字列を使用して Context クラスを初期化しています。また、Mainの先頭で、 データベースイニシャライザをカスタムのイニシャライザに変更しています。

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

namespace CrmEF4CF02
{
    class Program
    {
        static string connectionString = "Server=(local);Initial Catalog=dyn_MSCRM;Integrated Security=true;multipleactiveresultsets=true";
        static void Main(string[] args)
        {            
            // DBの存在チェックのみを行う カスタムIDatabaseInitializerをセット
            Database.SetInitializer<DynMSCRMContext>(new CheckDatabaseExistenceOnly<DynMSCRMContext>());

            Console.WriteLine("-- 偽装しない場合の例");
            using (DynMSCRMContext context = new DynMSCRMContext(connectionString))
            {
                foreach (var user in context.SysteUsers.Where(x => x.IsDisabled == false))
                {
                    Console.WriteLine("ユーザ名:{0}", user.Fullname);
                    Console.WriteLine("\t部署 :{0}", user.BusinessUnit.Name);
                }
            }
            Console.WriteLine();
            Console.WriteLine("-- 存在する人に偽装する");
            using (DynMSCRMContext context = new DynMSCRMContext(connectionString))
            {
                using (new CrmDbImpersonator(context.Database.Connection, new Guid("2F4DC458-50FA-E011-85FA-00155D0A0B14")))
                {
                    foreach (var user in context.SysteUsers.Where(x => x.IsDisabled == false))
                    {
                        Console.WriteLine("ユーザ名:{0}", user.Fullname);
                        Console.WriteLine("\t部署 :{0}", user.BusinessUnit.Name);
                    }
                }
            }
            Console.WriteLine();
            Console.WriteLine("-- 存在しない人に偽装する");
            using (DynMSCRMContext context = new DynMSCRMContext(connectionString))
            {
                using (new CrmDbImpersonator(context.Database.Connection, new Guid("0362BCBA-D2B5-4F3E-8622-323144D2A042")))
                {
                    foreach (var user in context.SysteUsers.Where(x => x.IsDisabled == false))
                    {
                        Console.WriteLine("ユーザ名:{0}", user.Fullname);
                        Console.WriteLine("\t部署 :{0}", user.BusinessUnit.Name);
                    }
                }
            }
            Console.WriteLine();
            Console.WriteLine("Enterを入力してください.");
            Console.ReadLine();
        }
    }
}

プログラムは完了です。偽装するユーザのGUIDは環境に合わせて変更する必要があります。ためしに実行するとそれっぽい動作をしたのでたぶんうまくできているんではないかと。

3. まとめ

今回の説明は以上です。偽装が行えるっぽいことを確認しました。

プラグインなどで使用する場合は、接続文字列はレジストリやパラメタから受け取るようにすれば構成ファイルが不要になります。カスタムデータベースイニシャライザの設定はベースクラスのプラグインのスタティックコンストラクタなりなんなりで行えばいいんでないかと思います。