きよくらの備忘録

「三日坊主と呼ばせない!日記」改め。主にソフトウェア開発関連の話題。

Hangfireを使ってみる (3):HangfireでDIコンテナを使ってみる

前回の続きです。

前回は『ClientはASP.NET Webアプリケーション、ServerをWindowsコンソールアプリケーションで実装』という形の実装に変更しました。

構成としては今回私がやりたい要件*1を満たせそうですが、一点、実現できていないことがありました。 それは、DLLとした切り出したジョブに対してServer(ジョブの実行プロセス)側から設定などの情報を差し込むことです。

例えばジョブの中でDBに接続したいのですが、その際の接続文字列もしくはDbContextなどは実行プロセス側で用意したものを利用したいわけです。

結果を先に言うと、DIコンテナを使うことで実現が可能でした。HangfireはDIコンテナを用いてIoCを実現するインフラがあり、また.NETのメジャーなDIコンテナを簡単に使うための拡張も提供されています。今回はDIコンテナとしてAutofacを使ってみました。

今回はその内容のメモになります。

 

今回やってみること

今回は、DIコンテナを使って『Hangfire Serverのプロセスからジョブ実行時にDB接続文字列を渡す』というのを実現してみます。

ソリューションは前回使ったものに手を入れていきます。

1. ジョブをインタフェースにと実装に切り分ける

まずはJOBをインタフェースと実装に分離します。さらに『Serverプロセスで実装する何らかのオブジェクトを受け取る』想定の実験(?)のために、設定値を格納するオブジェクト実装するためのインタフェース『IJobConfiguration』を作成し、『CustomJob.cs』はコンストラクタでその実装を受け取って利用するように書き換えます。

// IJobConfiguration.cs 【新規】
namespace MyJobsLib
{
  public interface IJobConfiguration
  {
    string ConnectionString { get; set; }
  }
}
// ICostomJob.cs 【新規】
namespace MyJobsLib
{
  public interface ICostomJob
  {
    void Execute(string message);
  }
}
// CustomJob.cs 【変更(主に★の箇所)】
using System.IO;

namespace MyJobsLib
{
  public class CustomJob : ICostomJob // ★ICostomJobの実装にする
  {
    // ★コンストラクタでIJobConfigurationの実装を受け取り保持することにする
    private IJobConfiguration JobConfiguration = null;

    public CustomJob(IJobConfiguration jobConfiguration)
    {
      JobConfiguration = jobConfiguration;
    }
    
    public void Execute(string message)
    {
      var fileName = string.Format("HangfireJob_{0}.txt", System.Guid.NewGuid());
      var filepath = Path.Combine(Path.GetTempPath(), fileName);
      using (var writer = new StreamWriter(filepath, true))
      {
        writer.WriteLine(message);

        // ★実行プロセス側から受け取った情報の接続文字列を使う……想定
        writer.WriteLine(JobConfiguration.ConnectionString);
      }
    }
  }
}

2. 【Hangfire Client側】 キュー登録処理を変更

BackgroundJob.Enqueueメソッドでキューにジョブを登録する際の記述を以下のように変更します。型引数にジョブのインタフェースを指定する記述になります。引数の書き方も変わります。

// HomeController.cs 【変更(主に★の箇所)】
using Hangfire;
using System.Web.Mvc;

namespace HangfireSample01.Web.Controllers
{
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      // Hangfireでjobをキューに登録
      BackgroundJob.Enqueue<MyJobsLib.ICostomJob>(x => x.Execute("Job Executed.")); // ★ここを変更
      return View();
    }

    public ActionResult About()
    {
      ViewBag.Message = "Your application description page.";

      return View();
    }

    public ActionResult Contact()
    {
      ViewBag.Message = "Your contact page.";

      return View();
    }
  }
}

3. 【Hangfire Server側】 DIコンテナを利用するように変更

最後にServer側です。ここは以下の対応を行います。

  1. IJobConfigurationを実装したクラスを実装
  2. Autofacに対応した拡張をNuGetでインストール
  3. コンテナの登録
IJobConfigurationを実装したクラスを実装する

IJobConfigurationを実装した、Serverのプロセス側からジョブに注入するオブジェクトのクラスを実装します。今回はそのままJobConfigurationとし、コンストラクタで設定値を積み込むようなものを想定したものにしてみました。

// JobConfiguration.cs 【新規】
namespace HangfireSample01.ServerConsole
{
  public class JobConfiguration : MyJobsLib.IJobConfiguration
  {
    public string ConnectionString { get; set; }

    public JobConfiguration()
    {
      ConnectionString = "接続文字列だよ";
    }
  }
}
Autofacに対応した拡張をNuGetでインストールする

NuGetパッケージ Hangfire.Autofac をインストールします。Autofac自体も自動的にインストールされます。パッケージマネージャーコンソールで行う場合は以下です。

PM > Install-Package HangFire.Autofac

コンテナの登録

最後にコンテナに型を登録し、コンテナの情報をHangfireのActivatorに設定する記述を行います。

// Program.cs 【変更(主に★の箇所)】
using Autofac;
using Hangfire;
using System;

namespace HangfireSample01.ServerConsole
{
  class Program
  {
    static void Main(string[] args)
    {
      var constr = @"Data Source=localhost;Initial Catalog=HangfireJob;Integrated Security=True";
      GlobalConfiguration.Configuration.UseSqlServerStorage(constr);

      // ★コンテナに型を登録、Activatorに設定
      var builder = new ContainerBuilder();
      builder.RegisterType<JobConfiguration>().As<MyJobsLib.IJobConfiguration>();
      builder.RegisterType<MyJobsLib.CustomJob>().As<MyJobsLib.ICostomJob>();
      GlobalConfiguration.Configuration.UseAutofacActivator(builder.Build());

      using (var server = new BackgroundJobServer())
      {
        Console.WriteLine("Hangfire Server started. Press any key to exit...");
        Console.ReadKey();
      }
    }
  }
}

以上でコーディングは完了です。

 

実行して確認する

さっそく実行して確認してみると、ダッシュボードで見えるジョブ名が「ICostomJob.Execute」とインタフェースでの記述に変わっています。またジョブによって作成されるファイルにも、Server側のプロセスから注入された値が反映されていることが分かります。

f:id:kiyokura:20170804012526p:plain

まとめ

HangfireはDIコンテナの利用が想定されていて、メジャーなDIコンテナの実装に対応した拡張も提供されています。(参照:Extensions/IoC Containers

実際に、特に面倒もなくDIコンテナを利用できることがお分かりいただけたのではないでしょうか。

サンプルソースは例によって以下のブランチstep03です。

github.com

これでいったんこのメモのシリーズは終了する予定ですが、この後もHangfireを使っていて調べたこと・ハマったこと等を随時メモしていくと思います。

*1:お仕事で取り組んでる事なので詳細は割愛

Hangfireを使ってみる (2):ClientとServerを別プロセスにしてみる

前回からの続きです。

前回は『ClientとServerの双方を同一のASP.NET Webアプリケーションに実装』しましたが、今回はこれを分離してみたいと思います。

Hangfire Clientと Hangfire Serverを別プロセスにする

今回は『ClientはASP.NET Webアプリケーション、ServerをWindowsコンソールアプリケーションで実装』を試してみます。またClientとServerを別にするだけでなく、ジョブとして実行する処理もDLLとして切り出しておくことにします。

f:id:kiyokura:20170803223227p:plain

ソリューションは前回使ったものに手を入れていきます。

 

実装手順

1. ジョブのDLLを作成

ソリューションにWindowsクラスライブラリのプロジェクト『MyJobsLib』を追加して、ジョブとして実行したい処理のクラスを実装します。ジョブはCustomJob.csとして、実行結果が分かり易いようにTempフォルダにテキストファイルを吐き出すような処理にしてみました。

f:id:kiyokura:20170803232927p:plain

// CustomJob.cs
using System.IO;

namespace MyJobsLib
{
  public class CustomJob
  {
    public void Execute(string message)
    {
      var fileName = string.Format("HangfireJob_{0}.txt", System.Guid.NewGuid());
      var filepath = Path.Combine(Path.GetTempPath(), fileName);
      using (var writer = new StreamWriter(filepath, true))
      {
        writer.WriteLine(message);
      }
    }
  }
}
2. Webアプリケーションの書き換え

Webアプリケーション側は主に以下の2点対応を行います。

  1. キューに登録するジョブを習性
  2. OWIN StartUpからHangfire Serverの登録を削除

まず、[参照の追加]等からMyJobsLibを参照したうえで、BackgroundJob.Enqueueで登録するジョブをDLL内のものに変更します。

// HomeController.cs
using Hangfire;
using System.Web.Mvc;

namespace HangfireSample01.Web.Controllers
{
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      // Hangfireでjobをキューに登録
      BackgroundJob.Enqueue(() => new MyJobsLib.CustomJob().Execute("Job Executed.")); // ★ここを変更
      return View();
    }

    public ActionResult About()
    {
      ViewBag.Message = "Your application description page.";

      return View();
    }

    public ActionResult Contact()
    {
      ViewBag.Message = "Your contact page.";

      return View();
    }
  }
}

次に、Statup.csでHangfire Serverの登録を行っていた箇所を削除します。

// Startup.cs
using Hangfire;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(HangfireSample01.Web.Startup))]

namespace HangfireSample01.Web
{
  public class Startup
  {
    public void Configuration(IAppBuilder app)
    {
      var constr = @"Data Source=localhost;Initial Catalog=HangfireJob;Integrated Security=True";
      GlobalConfiguration.Configuration.UseSqlServerStorage(constr);

      app.UseHangfireDashboard();
      // app.UseHangfireServer(); ★ここを削除
    }
  }
}
3. コンソールアプリケーションの作成

最後に、Hangfire Serverとして動作させるコンソールアプリケーションを作成します。ここはオフィシャルのドキュメントProcessing jobs in a console applicationを参考に……というかほぼそのままです。

コンソールアプリケーションプロジェクト追加

ソリューションに『HangfireSample01.ServerConsole』という名前でコンソールアプリケーションプロジェクトを追加しました。

f:id:kiyokura:20170803234729p:plain

ジョブのDLLを参照する

[参照の追加]等からMyJobsLibの参照を追加します。ちなみにこの追加を行わなくてもビルドでエラーが出ないので、うっかり忘れると実行時に例外に遭遇することになります(経験者談)。

NuGetパッケージをインストール

サーバーとして動作させるだけの場合、必要なパッケージは限られるます。オフィシャルにドキュメントに従い、Hangfire.CoreHangfire.SqlServerのみをインストールしました。パッケージマネージャーコンソールから行う場合は以下の操作になります。

PM> Install-Package Hangfire.Core

PM> Install-Package Hangfire.SqlServer

Hangfire Serverの起動処理を実装

最後にProgram.csにHangfire Serverの起動処理を書きます。UseSqlServerStorageメソッドの引数に渡す接続文字列は、Webアプリケーション側で指定したのと同じものを指定します。それ以外はオフィシャルのドキュメントのそのままです。

// Program.cs
using Hangfire;
using System;

namespace HangfireSample01.ServerConsole
{
  class Program
  {
    static void Main(string[] args)
    {
      var constr = @"Data Source=localhost;Initial Catalog=HangfireJob;Integrated Security=True";
      GlobalConfiguration.Configuration.UseSqlServerStorage(constr);

      using (var server = new BackgroundJobServer())
      {
        Console.WriteLine("Hangfire Server started. Press any key to exit...");
        Console.ReadKey();
      }
    }
  }
}

 

実行して確認する

以上で準備が完了したので早速実行してみます。

……と言いつつ、簡単に実行するために実行前にもう一つ設定しておきます。ソリューションの[スタートアッププロジェクトの設定]で、コンソールアプリケーションとWebアプリケーションの両方を実行するように設定しておきます。

f:id:kiyokura:20170803235821p:plain

 

この状態でF5でデバッグ実行を開始するとブラウザとコンソールアプリケーションの両方が起動すると思います。起動したらHangfireのダッシュボードを表示し、jobの状態を確認してみると、ちゃんとジョブが実行されていることが確認できました。

f:id:kiyokura:20170804000620p:plain

また、Tempフォルダにファイルが作成されていることも確認できました。

f:id:kiyokura:20170804000549p:plain

 

ここまでのまとめ

ClientとServerの分離を試してみました。今回はConsoleアプリケーションでやりましたが、他の形態でも基本は変わらないように思います。またオフィシャルのドキュメント Processing jobs in a Windows Service にはWindowsサービスでの実装について書かれています。

今回のソースは以下のリポジトリのstep02ブランチになります。

github.com

次回は、DIコンテナを用いてさらに実用的な構成の検証をしていきます。

Hangfireを使ってみる (1):とりあえず触ってみる

最近 Hangfire について調査・検証しているのでメモ。

Hangfireとは

Hangfireは.NET向けのバックグランド・タスクのライブラリです。

www.hangfire.io

ストーレージにタスクをキューイングして、非同期に実行させることができます。即時実行だけでなく、cronっぽい感じで定期実行したりもできる模様。 OWIN Middleware としてASP.NETで利用できるほか、コンソール/WPF/Windows Form/Windows サービス等など……でも使えます。

 

今回から予定している一連のエントリは『ASP.NETのWebアプリケーションから重たい処理(例:集計帳票作成処理)を非同期実行させる』という観点での調査検証に基づいています。ですので、ある程度方より(?)がある点はご了承ください。

Hangfireを構成するコンポーネント

Hangfireはざっくりと以下のようなコンポーネントで構成されています。

コンポーネント 役割 典型的な実装
Hangfire Client Jobをキューへ登録 ASP.NET等, ユーザーが操作するアプリ
Job Storage Jobのキュー,その他管理情報を格納 SQL Server / Redis 等
Hangfire Server キュー上のJobの実行 ASP.NET(バックグラウンド) / Windowsサービス等

 

Hangfire ClientとHangfire Serverは同一のプロセス上で実装可能です*1。 そのほか、ASP.NET用にビルトインされていてるダッシュボード機能があり、jobの状況のモニタリングや再実行などの操作を行うことができます。

とりあえず試してみる

チュートリアル的に一番シンプルで試しやすい構成として、『ClientとServerの双方を同一のASP.NET Webアプリケーションに実装』するパターンを試してみます。

f:id:kiyokura:20170803113448p:plain:w300

https://github.com/kiyokura/HangfireSample01/tree/step01

基本構成

とりあえずこんな感じでやってみました。

  • Visual Studio 2015
  • ASP.NET MVC 5
  • SQL Server 2016

実装手順

1. SQL ServerでHangfire用のDBを作成

Jobストレージとして利用するデータベースが必要なので適当に作成しておきます。データベース内にはHangfireスキーマといくつか専用のテーブルが作成されます*2

2. ASP.NET MVCのプロジェクトを作成する

Visual Studio 2015で適当にASP.NET MVCのプロジェクトを作成しました。 f:id:kiyokura:20170803115757p:plain:w300

3. NuGetでHangfireのパッケージを取得

NuGetでHangfireを取り込みます。コマンドラインからだとInstall-Package HangfireでOK。

f:id:kiyokura:20170803120224p:plain

4. OWINスタートアップを記述

Hangire の最低限必要な設定をOWINスタートアップで行うように記述します。Webアプリケーションプロジェクトの直下にOWIN Startupクラス「Startup.cs」を追加して、以下のように書きました。

using Hangfire;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(HangfireSample01.Startup))]

namespace HangfireSample01
{
  public class Startup
  {
    public void Configuration(IAppBuilder app)
    {
      // 接続文字列は環境に応じて変えること
      var constr = @"Data Source=localhost;Initial Catalog=HangfireJob;Integrated Security=True";
      GlobalConfiguration.Configuration.UseSqlServerStorage(constr);

      app.UseHangfireDashboard();
      app.UseHangfireServer();
    }
  }
}
5. Jobの登録処理を記述

最後に実際にjobをキューに登録する処理を書きます。既存のHomeControllerのIndexアクションメソッドに、とりあえずコンソールに文字を出力するだけの単純な処理をjobとして登録するように書いてみました。

using Hangfire;
using System;
using System.Web.Mvc;

namespace HangfireSample01.Controllers
{
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      // Hangfireでjobをキューに登録
      BackgroundJob.Enqueue(() => Console.WriteLine("Simple Job"));
      return View();
    }

    public ActionResult About()
    {
      ViewBag.Message = "Your application description page.";
      return View();
    }

    public ActionResult Contact()
    {
      ViewBag.Message = "Your contact page.";

      return View();
    }
  }
}

実行して確認する

準備が終わったので実行してみます。

1.WebアプリケーションをF5で実行

F5でデバッグ実行を開始すると見慣れた(?)ASP.NET MVCのテンプレートのホーム画面が表示されます。このとき、Home/Indexアクションメソッドが実行されているので裏ではキューにjobが登録され、そして実行されているはずです。

次の手順でそのあたりを確認してみます。

2.Hangfireダッシュボードを表示

Hangfire組み込みのダッシュボードを利用してjobの状況を確認してみます。hangfireダッシュボードは、アプリケーションルート+/hangfireというURLでアクセスできます。今回のようにIIS Expressでローカル実行している場合は例えば以下のようになります

  • http://localhost:61828/hangfire (※ポート番号は環境によって変わります)

アクセスできるとこんな感じのダッシュボードが表示されます。 f:id:kiyokura:20170803122544p:plain

3.jobの実行状況を確認

ダッシュボードで実際のjobがどのようになっているかを確認します。 メニューから[jobs]-[Succeeded]とたどっていくと、成功したジョブ一覧に先ほど登録したjobが並んでいるのが見えました。

f:id:kiyokura:20170803122848p:plain

さらにjob名をクリックすると詳細情報が見れます。 f:id:kiyokura:20170803123351p:plain

4.データベース(Job Storage)を確認

最後に、Job Storageとなっているデータベースを簡単に確認してみました。

SSMSで該当のデータベースを開いてみると、こんな感じでテーブルが作成され、例えばHangfire.Jobテーブルにはこんな形でJobが格納されているのが分かります。 f:id:kiyokura:20170803123619p:plain

ここまでのまとめ

Hangfireのクイックスタート的な感じでとりあえず手元で基本的な動作をさせてみました。というか、オフィシャルのドキュメントのQuick Startにあるそのままです。

なお、ここまでのサンプルコードは以下にあります

github.com

このHangfire ServerもASP.NETのインプロセスで動かすモデルの場合、ASP.NETの標準仕組の HostingEnvironment.QueueBackgroundWorkItem と似ているようにも見えます。しかしキューが永続化されているためjobのトレースやリトライ等QueueBackgroundWorkItemでは実現困難なことが簡単に実現できる点や将来的なスケールアウトの容易さ等、差別化のポイントはいくつもあるように思います。 (一方QueueBackgroundWorkItemはインプロセス前提なのでHttpContext等ASP.NETの実行インスタンスに依存する情報をHangfire よりもよりシンプルに扱うことができる等のメリット?はあかなーと思ったりもします)

次はClientとServerを別プロセスでやってみたいと思います。

*1:もちろん別のプロセスに分離できます。スケールなど考えるとそちらのほうが良いケースは少なくないでしょう

*2:各オブジェクトは初回実行時に勝手に作成されるのでこのタイミングで手動で作成する必要はありません

SQL Server Data Toolsのユニットテスト実行前にLocalDBのインスタンスを破棄・再作成する

SQL Server Data Tools (SSDT)のユニットテスト機能ネタです。

テスト実行前の自動デプロイは便利だが失敗するケースがある

以前の記事(これとかこれ)でも触れた、SSDTの ユニットテスト実行前にDBインスタンスに自動でデプロイを行う機能 は非常に便利です。DBプロジェクト側で変更を普通に保存した後、テストランナーでテストを走らせるだけで環境を最新にして実行してくれます。

便利なのですが、この自動デプロイが必ず失敗するケースがあります。

例えばデータが入っているテーブルの列を削除したり列の型を変更するなどデータ消失が発生する可能性があるような変更がある場合です*1

もちろん一旦手動で削除してから実行することはできますが、手間ですしCIでの自動テストの障害にもなります。

DBプロジェクトの発行機能では発行プロファイルの設定でデータベースを毎回再作成するオプションがありますが、どうやらテスト実行時の発行には利用できないようです。

再実行時に単純に削除して再作成すればよいのであれば*2、SQL Server Express LocalDB(以下LocalDB)と組み合わせて以下のような方法を思いつきました。

 SQL LocalDB Wrapper を使ってデプロイ前にLocalDBのインスタンスを削除・再作成する

LocalDBの場合、簡単にインスタンスを削除するすることができます。

またSQL Server LocalDBのインスタンスを削除してもユーザーデータベースのファイルが残る でも紹介した SQL LocalDB Wrapperを使うと、LocalDBのインスタンスをSSDTの自動デプロイで作成されたデータベースごときれいに削除できます。SQL LocalDB Wrapperにはインスタンスを再作成する機能もあります。

これらを利用すると、テストの初期化時、データベースのデプロイが始まる前にLocalDBのインスタンスを破棄・再作成が簡単に行えます。

組み込み例

組み込み方は非常に簡単です。テストプロジェクトにNuGetでSQL LocalDB Wrapperを取り込んだ後、例えば、適当に以下のようなメソッドを作ってやります。

/// <summary>
///  LocalDBのインスタンスの再作成
/// </summary>
/// <remarks>
/// SQL LocalDB Wrapper経由でネイティブAPIを通して操作する
/// https://www.nuget.org/packages/System.Data.SqlLocalDb/
/// </remarks>
internal void RecreateLocalDbInstance(string instanceName, string version)
{
  // インスタンスが存在したらインスタンスを停止してからユーザーDBも同時に削除してから作成
  if (SqlLocalDbApi.GetInstanceNames().Contains(instanceName))
  {
    SqlLocalDbApi.StopInstance(instanceName, StopInstanceOptions.KillProcess, new TimeSpan(0, 0, 30));
    SqlLocalDbApi.DeleteInstance(instanceName, true);
  }
  SqlLocalDbApi.CreateInstance(instanceName, version);
}

余談ですが、SqlLocalDbApi.CreateInstance(String, String)の第二引数を渡さない(オーバーロードのSqlLocalDbApi.CreateInstance(String)を呼ぶ)と、作成されるインスタンスのバージョンは環境にインストールされている最新のLocalDBにものになるようです(ざっとソースを見た感じ)。なので指定しなくても問題はないのですが、たとえば複数人による開発で実行環境にばらつきがあるような場合は、ここはあえて指定するようにしておいたほうが無難です。この詳細はまた別途改めて書きたいと思っています。*3

あとは先ほど作ったRecreateLocalDbInstanc()をSqlDatabaseSetup.csのSqlDatabaseSetup.InitializeAssemblyメソッドの頭のほうで呼んでやればOKです。

  [TestClass()]
  public class SqlDatabaseSetup
  {
    [AssemblyInitialize()]
    public static void InitializeAssembly(TestContext ctx)
    {
        RecreateLocalDbInstance("SomeDbInstance","13.0");

     // ...既存の処理はそのまま
    }
  }

まとめ

SQL LocalDB Wrapperを使うとC#のコード中からLocalDBを簡単に操作出来て便利ですね。 まだぼんやりとしか考えてないのですが、アプリケーションのユニットテストでDBに接続する必要があるようなテストでもこれを利用していい感じにできるんじゃないかとか思ったりしてます。

*1:テスト自体にトランザクションを利用してデータが残らないような状態にキープしておくのであればこの問題は発生しません。しかし、例えばバッチ更新のストアドのテストで失敗したときにテーブルの状態を確認したい場合や、値が原則変わらないマスタ類をテスト開始時に投入ておきたいなどの要件と相容れません

*2:単体テストなのでそうあるべきとは思う

*3:何時になるかわからないので簡単にメモしておきます。SQL Server 2014以前のデータベースのコンポーネントが入っている環境で、バージョン13.0(SQL Server 2016相当)のLocalDBのインスタンスへのテスト実行前自動デプロイが失敗することがありました。SQL Server 2016のSSMSをインストールしたり、LocalDBのインスタンスのバージョンを12.0で作成するとうまくいくことから、13.0のLocalDBに対して12.0までしか対応していないコンポーネントを使って接続しようとして失敗してるように見えました。もう少し状況を絞りたいので現状はこれ以上は追加調査できていません。

真・SQL Server Data Toolsのユニットテスト実行前に複数DBにデプロイする

前回の続きです。

前回のエントリに対して、@ahiru_sp さんからアドバイスいただきました。

なるほど。リファレンスにあるprotected static な'DeployDatabaseProject(String, String, String, String)'のを使えばいいのではということですね。

SqlDatabaseTestService.DeployDatabaseProject Method (Microsoft.Data.Tools.Schema.Sql.UnitTesting)

 

SqlDatabaseTestServiceの派生クラスを作成する

ということで、そのままやってみます。

まず以下のような Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestService のは派生クラスを作り、DeployDatabaseProject(String, String, String, String)を呼ぶメソッドを一つ作っておきます。

internal class CustomSqlDatabaseTestService : SqlDatabaseTestService
{
  /// <summary>
  /// DBのデプロイ
  /// </summary>
  /// <param name="projectFile">プロジェクトファイルのパス(相対パス)</param>
  /// <param name="projectConfiguration">プロジェクトファイルの構成設定</param>
  /// <param name="connectionString">接続文字列</param>
  public void DeployDatabaseProjectEx(string projectFile, string projectConfiguration, string connectionString)
  {
    SqlDatabaseTestService.DeployDatabaseProject(projectFile, projectConfiguration, "System.Data.SqlClient", connectionString);
  }
}

引数に何を渡すかは、app.configSqlUnitTestingセクションをみれは推測はできますね。なお、第三引数はリファレンスに This must be System.Data.SqlClient. って明記されてるので埋め込んでおきます(まあどうせSQL Serverだし)。

あとは、昨日のやつと同様、InitializeAssemblyから呼んでやればOKです。例えばこんな感じ。

using System;
using System.Configuration;
using Microsoft.Data.Tools.Schema.Sql.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Xml;

namespace BletillaTestsNext
{
  [TestClass()]
  public class SqlDatabaseSetup
  {
    // ターゲットデータベース
    const string TARGET_DB_PROJECT = "..\\..\\..\\ MainDb\\MainDb.sqlproj";
    const string TARGET_DB_CONFIGURATION = "Release";
    const string TARGET_DB_CONNECTION_STRING = "<デプロイ先のDBの接続文字列>";
   
    // 依存データベース
    const string DEPEND_DB_PROJECT = "..\\..\\..\\SubDb\\SubDb.sqlproj";
    const string DEPEND_DB_CONFIGURATION = "Release";
    const string DEPEND_DB_CONNECTION_STRING = "<デプロイ先のDBの接続文字列> ";

    [AssemblyInitialize()]
    public static void InitializeAssembly(TestContext ctx)
    {
      var testService = new CustomSqlDatabaseTestService();

      // テスト実行ターゲットをデプロイ
      testService.DeployDatabaseProjectEx(TARGET_DB_PROJECT, TARGET_DB_CONFIGURATION, TARGET_DB_CONNECTION_STRING  );
 
      // 依存DBをデプロイ
      testService.DeployDatabaseProjectEx(DEPEND_DB_PROJECT, DEPEND_DB_CONFIGURATION , DEPEND_DB_CONNECTION_STRING);

      // もともとあったこれは現在は使われてないので削除して問題ない
      //SqlDatabaseTestClass.TestService.GenerateData();
    }
  }
}

実際に試してみたら、あっさりそのまま動きました。なんだかすっきりしましたし無理やりapp.configを書き換えるとかするよりもこちらの方が断然スマートですね。

@ahiru_sp さん、ありがとうございました!