C#、ASP.NET、TypeScript、Angular を中心にプログラミングに関した話題を諸々。
by @jsakamoto
検索
リンク
北海道のITコミュニティ - CLR/H 無聊を託つ
タグ
カテゴリ
最新の記事
C# での Web アプリ開..
at 2021-09-21 20:40
MSBuild スクリプトで..
at 2021-08-23 21:10
Entity Framewo..
at 2021-07-29 18:11
[解決] .NET 5.0 ..
at 2021-06-30 20:48
Entity Framewo..
at 2021-05-31 19:07
最新のコメント
記事当時はEPPlusの..
by 通行人 at 17:26
EPPlusのライセンス..
by ライセンス確認者 at 11:29
"ウソはよくない" との..
by developer-adjust at 19:01
C#スクリプト+VSco..
by ウソはよくない at 21:00
Hyper-vのマウス問..
by macin at 15:54
Windows Upda..
by mtakama at 12:06
同じ問題で悩んでいました..
by yonas at 12:32
Mac(Chrome)で..
by Macユーザー at 00:46
すみません、記事書きかけ..
by developer-adjust at 22:36
続きはないんですか?
by hanamo at 13:03
記事ランキング
最新のトラックバック
Web API における..
from 松崎 剛 Blog
[Other]Code2..
from KatsuYuzuの日記
Developer @ ..
from .NET Clips
asp.netでrail..
from 4丁目より
F#でASP.NET M..
from ナオキにASP.NET(仮)
[B!] これはいい h..
from Twitter Mirror
[報告] Microso..
from .NET Clips
Developer @ ..
from .NET Clips
[F#]F# でブログア..
from 予定は未定Blog版
ASP.NET MVC ..
from ナオキにASP.NET(仮)
以前の記事
2021年 09月
2021年 08月
2021年 07月
2021年 06月
2021年 05月
2021年 04月
2021年 03月
2021年 02月
2021年 01月
2020年 12月
2020年 11月
2020年 10月
2020年 09月
2020年 08月
2020年 07月
2020年 06月
2020年 05月
2020年 04月
2020年 03月
2020年 02月
2020年 01月
2019年 12月
2019年 11月
2019年 10月
2019年 09月
2019年 08月
2019年 07月
2019年 06月
2019年 05月
2019年 04月
2019年 03月
2019年 02月
2019年 01月
2018年 12月
2018年 11月
2018年 10月
2018年 09月
2018年 08月
2018年 07月
2018年 06月
2018年 05月
2018年 04月
2018年 03月
2018年 02月
2018年 01月
2017年 12月
2017年 11月
2017年 10月
2017年 09月
2017年 08月
2017年 07月
2017年 06月
2017年 05月
2017年 04月
2017年 02月
2017年 01月
2016年 12月
2016年 11月
2016年 10月
2016年 09月
2016年 08月
2016年 07月
2016年 06月
2016年 05月
2016年 04月
2016年 03月
2016年 02月
2016年 01月
2015年 12月
2015年 11月
2015年 10月
2015年 09月
2015年 08月
2015年 07月
2015年 05月
2015年 04月
2015年 03月
2015年 02月
2015年 01月
2014年 12月
2014年 11月
2014年 10月
2014年 09月
2014年 08月
2014年 06月
2014年 04月
2014年 03月
2014年 02月
2014年 01月
2013年 12月
2013年 10月
2013年 09月
2013年 08月
2013年 07月
2013年 06月
2013年 05月
2013年 04月
2013年 03月
2013年 02月
2013年 01月
2012年 12月
2012年 11月
2012年 10月
2012年 09月
2012年 08月
2012年 07月
2012年 06月
2012年 05月
2012年 04月
2012年 03月
2012年 02月
2012年 01月
2011年 12月
2011年 11月
2011年 10月
2011年 09月
2011年 08月
2011年 07月
2011年 06月
2011年 05月
2011年 04月
2011年 03月
2011年 02月
2011年 01月
2010年 12月
2010年 11月
2010年 10月
2010年 09月
2010年 08月
2010年 07月
2010年 06月
2010年 05月
2010年 04月
2010年 03月
2010年 02月
2010年 01月
2009年 12月
2009年 10月
2009年 09月
2009年 07月
2009年 06月
2009年 05月
2009年 04月
2009年 03月
2009年 02月
2009年 01月
2008年 12月
2008年 11月
2008年 10月
2008年 09月
2008年 08月
2008年 07月
2008年 06月
2008年 05月
2008年 04月
2008年 03月
2008年 02月
2008年 01月
2007年 12月
2007年 11月
2007年 04月
2007年 03月
2007年 02月
2007年 01月
2006年 11月
2006年 10月
2006年 09月
2006年 08月
2006年 07月
ファン
ブログジャンル
画像一覧
2021年 07月 29日

Entity Framework Core + Code Style で、指定名のマイグレーションまでに留めてマイグレーションする

ライブラリ Entity Framework Core (以下、EFCore) を使って、SQL Server などのリレーショナルデータベースに読み書きする、C# プログラミングにおける話。

とりわけ、"Code First" と呼ばれる、C# コード側でデータベースの構造 (テーブルや列) を記述し、その記述に従ってデータベース側を自動で構築・更新 (マイグレーション) する、開発運用スタイルにおける話題である。

EFCore におけるマイグレーションについて詳しくは、公式ドキュメントを参照されたし。

さてさて、通常、EFCore におけるデータベースのマイグレーションは、以下のようなコードで実行される。
await dbContext.Database.MigrateAsync();

上記コードが実行されると、このデータベースコンテキストの接続先データベースにあるマイグレーションの履歴テーブルに記録されてる履歴と、実行中のプログラムに定義されているマイグレーションの定義とを照合する。

そして、未適用のマイグレーションが実行中のプログラムに定義されていれば、それら未適用のマイグレーション定義を適用する。

こうして、実行中のプログラムで定義されているデータベース構造と、実際のデータベース構造とが整合されて動作するようになる。

ちょっと奇妙な要件発生


さてある日、かなり奇妙ではあるが、未適用のマイグレーション定義をすべて適用するのではなく、指定のマイグレーション定義までに留めて適用したいという要件が発生してしまった。

つまり、とある C# プログラム内に、"M1", "M2", "M3" という3段階のマイグレーション定義が含まれているが、適用するのは "M2" までに留めたい、という要件だ。
このプログラムを実行したときに、接続先データベースに適用済みのマイグレーションと、期待される動作との対応は以下のとおりである。

  • 接続先データベースに適用済みのマイグレーションが "M1" ⇒ "M2" を適用
  • 接続先データベースに適用済みのマイグレーションが "M2" ⇒ なにもしない
  • 接続先データベースに適用済みのマイグレーションが "M3" ⇒ なにもしない

あいにくと先に記した "MigrateAsync()" メソッドを実行すると、そのプログラムに含まれるすべての未適用マイグレーション定義が適用されてしまう (上記例でいうと、いずれのシナリオでも "M3" まで適用されてしまう) ので、この要件を満たせず困ってしまった、という話だ。

何ともややこしい話で、なぜそのような需要が発生するのか見当もつかない読者も多いと思うし、もしかするともっと別の作戦・戦略で解決すべき課題だったのかもしれない。
しかし書き始めると長くなるので、ここは詳細割愛し、そういう要件が発生してしまった、という前提のまま話を進める。

EFCore に、ちゃんと用意があった


さてこのような要件を達成することができるのか、というと、いちおう、ちゃんと (?) EFCore 側に用意がある。

まず知っておくべき事として、実は、dbContext.Database オブジェクトは、IInfrastructure<IServiceProvider> インターフェースを実装している。
このインターフェース経由で dbContext.Database オブジェクトに問い合わせすると、dbContext.Database オブジェクトが隠し持っている (?) 各種サービスの参照を手に入れることができる。

そして、それらサービスのひとつとして、EFCore におけるマイグレーションの諸々を司る IMigrator インターフェースのサービスがある。

この IMigrator インターフェースには、指定の名前のマイグレーション定義にまでマイグレーションを進める、引数に対象マイグレーション定義名を持つ、"MigrateAsync(string)" (又はその同期バージョンである "Migrate(string)") メソッドが用意されているのだ。

具体的なコード例を書くと以下のような感じになる。
var services = dbContext.Database as IInfrastructure<IServiceProvider>;
var migrator = services.GetService<IMigrator>();
await migrator.MigrateAsync("M2");

これで指定したマイグレーション定義、上記例だと "M2" までのマイグレーション適用が可能となる。

もう一捻り


だがしかし、まだ上記コードでは足りない点がある。

というのも、IMigrator.MigrateAsync(string) メソッドは、指定されたマイグレーション定義まで、マイグレーション適用を "進める" だけでなく "戻す" 作用もあるからだ。
つまり、上記コード例だと、以下のように動作してしまう。

  • 適用済みのマイグレーションが "M1" ⇒ "M2" を適用
  • 適用済みのマイグレーションが "M2" ⇒ なにもしない
  • 適用済みのマイグレーションが "M3" ⇒ "M3" をロールバック ("M2"適用相当にまで戻る)

言い換えると、IMigrator.MigrateAsync(string) メソッドは、指定されたマイグレーション定義までを適用したのと同じレベルに、接続先データベースの構造を整合させようとするわけだ。

もちろん、この動作が望ましいシナリオも多々あると思う。
しかし今回の要件は、すでに "M2" までが適用すみであれば、接続先データベースが実際には "M3" まで進んでいたとしても、何もしないことが要件だ。
そこで今回要件に対応するためには、もう一捻りが必要、というわけである。

幸い、これは簡単である。

接続先データベースに適用済みのマイグレーション定義名は、GetAppliedMigrations() というメソッドでマイグレーション定義名文字列の集合として取得できる。
これと照合して、希望のマイグレーションが適用済みでない場合に限って指定マイグレーションまでを適用、とすればよい次第。

具体的なコード例を以下に示す。
var appliedMigratiosn = dbContext.Database.GetAppliedMigrations();

// "M2" マイグレーションが、適用済みマイグレーション一覧にない場合...
if (!appliedMigratiosn.Contains("M2")){

  // 以下は先に紹介のコードに同じ
  var services = dbContext.Database as IInfrastructure<IServiceProvider>;
  var migrator = services.GetService<IMigrator>();
  await migrator.MigrateAsync("M2");
}

以上のコードで、今回要件に対応完了とすることができた。

おわりに


今回要件の話は極めて特殊であろう。

しかしながら、
  • DbContext.Database オブジェクトが IInfrastructure<IServiceProvider> インターフェースを実装している事であるとか、
  • そのサービスプロバイダから EFCore 管理下のサービスオブジェクトを入手できること、
  • IMigrator インターフェースをもつサービスオブジェクトの存在
などなど、今回のエピソードを通して、何かしらピンチの時にヒントになりそうな知見を手に入れられたと思う。


# by developer-adjust | 2021-07-29 18:11 | .NET | Comments(0)
2021年 06月 30日

[解決] .NET 5.0 の xUnit 単体テストプロジェクトに app.config を配置しても読み取れない

C# による Windows OS 用のアプリケーション開発における話。

.NET Framework 製の古いプログラムを .NET5.0 に移植を進めたりすることがある。
そのような古いプログラムでは、System.Configuration.ConfigurationManager クラスを使い、その静的プロパティ AppSettings[] を参照して、アプリケーション構成を読み取る実装だったりする。

ここでいうアプリケーション構成は、<.exeファイル名>.config という XML 形式のファイル中、"appSettings" というノード内のエントリとして設定される。
例えば下記のような XML だ。
<configuration>
  <appSettings>
    <add key="foo" value="bar"/>
  </appSettings>
</configuration>
このような <.exeファイル名>.config がある場合、その .exe を実行したときには、ConfigurationManager.AppSettings["foo"] を参照すると "bar" が返る、といった次第。

また、プロジェクトフォルダ直下に App.config というファイル名で構成ファイルを置いておけば、ビルド時に自動で出力フォルダに <.exe ファイル名>.config というファイル名でコピーされるようになっている。

xUnit 単体テストでアプリケーション構成が読み取れない


さてこのような古いプログラムの .NET 5.0 化を進めるにあたり、アプリケーション構成の読み取りも IOptions<T> ベースのモダンな方式に移行するのが本来なのだろう。

だが、諸事情でなかなかそうもいかないこともある。

ということで、ある日とある案件にて、ConfigurationManager.AppSettings[] を使う方式のまま、xUnit による単体テストを追加するという、希な機会に恵まれ (?) た。 

ということで、xUnit 単体テストを作成し、その単体テストプロジェクト直下に App.config ファイルを配置。
[解決] .NET 5.0 の xUnit 単体テストプロジェクトに app.config を配置しても読み取れない_d0079457_20182403.png

しかしこれで実行したところ、当初の期待に反し、問題発生。
その App.config の内容がテストコード内で読み取れなかったのだ。

原因は?


このテストプロジェクトの出力フォルダを参照してみたところ、下図のように、構成ファイルのファイル名は "TestProject1.config" となっていた。

[解決] .NET 5.0 の xUnit 単体テストプロジェクトに app.config を配置しても読み取れない_d0079457_20232701.png

しかし上図を見るとわかるように、どうやら単体テスト実行時のプロセス名 (.exe 名) は "testhost.exe" であるらしいことがわかる。

更に言うと、実は上図で見えている "testhost.exe" はどうやら "ドライバー" とよばれる .NET Core プログラムのブートストラッパーであるらしく、単体テスト実行プログラム本体は "testhost.dll" とのこと。

すなわち、System.Configuration.ConfigurationManager が参照する .config ファイル名は、"testhost.dll.config" ということになる。

ところが、ビルドシステムの都合上、プロジェクトフォルダ直下においた App.config ファイルは、単体テストアセンブリファイル名 + .config で出力フォルダにコピーされる。
そのため、上記事例では .config ファイル名が "TestProject1.dll.config" となってしまった。

以上のカラクリのために、単体テストコード中から App.config に設定した内容を読み取れない (参照する .config ファイル名が、単体テスト実行時のプロセス名と合致しないから) ということになるようだ。

解決


ということで、原因がわかってしまえば、対処は簡単だ。

単体テストプロジェクトフォルダ直下に配置する構成ファイルのファイル名を、App.config ではなく、"testhost.dll.config" とし、出力フォルダへのコピーの設定を "新しければコピー" に設定しておくのだ。

[解決] .NET 5.0 の xUnit 単体テストプロジェクトに app.config を配置しても読み取れない_d0079457_20381888.png

これで xUnit 単体テストコード中からも、期待したとおり、.config ファイルの内容を読み取れるようになった。






# by developer-adjust | 2021-06-30 20:48 | .NET | Comments(0)
2021年 05月 31日

Entity Framework Core で「勇者が左右の手に持つ装備を、装備マスタから選択する」モデルを実装する方法

架空のシナリオ -「勇者の冒険ゲーム」を制作中


リレーショナルデータベースへのアクセスに Entity Framework Core を使った、Code First スタイルによる、C# プログラミングの話。

架空のシナリオとして、「勇者の冒険ゲーム」を制作中だとする。
このゲームのモデリングにおいて、まず、以下のような「装備マスタ」のエンティティ型を用意する。
public class Equipment 
{
  public int Id { get; set; }

  [Required, StringLength(20)]
  public string Name { get; set; }
}
そして「勇者 (プレイヤー) 」は、左右の手それぞれに、上記「装備マスタ」に登録されている装備 (レコード) のうち、任意のひとつを持つものとする。
そのような「勇者 (プレイヤー)」のエンティティ型を、以下のように実装した。
public class Player
{
  public int Id { get; set; }

  // 左の手に持つ装備の装備マスタ上の ID
  public int LeftEquipmentId { get; set; }

  public virtual Equipment LeftEquipment { get; set; }

  // 左の手に持つ装備の装備マスタ上の ID
  public int RightEquipmentId { get; set; }

  public virtual Equipment RightEquipment { get; set; }
}
あとは上記2つのエンティティ型を使って、データベースコンテキスト型を構築すればよい。
public class MyGameDbContext : DbContext
{
  public DbSet<Equipment> Equipments { get; set; }
  public DbSet<Player> Players { get; set;}
  ...
}
以上のように実装することで、例えば以下のような感じで、「勇者 (プレイヤー)」のもつ装備を容易に参照可能となる。
var player = myGameAppDbContext.Players
  .Inclide(p => p.LeftEquipment)
  .Inclide(p => p.RightEquipment)
  .First(p=> ...);

// 👇 左手に持つ装備の名前が表示される
Console.WriteLine(player.LeftEquipment.Name);
各プロパティ名の命名則により、Entity Framework Core がよしなにエンティティモデルを構築してくれるおかげで、「装備マスタ」テーブルと「勇者 (プレイヤー)」テーブルとの、この2つのテーブル間の結合を、"ナビゲーションプロパティ" を参照するだけで直感的なコードで辿れるので便利である。

最後に、Code First スタイルなので、上記エンティティ周りの実装からデータベースを構築するよう、プログラムの開始時に以下のコードを実装する。
myGameAppDbContext.Database.EnsureCreated();

さてこれでよしよしと、ビルドもエラーなく成功。

さて、自分の場合はリレーショナルデータベースに SQL Server を使っていたので SQL Server に接続するよう構成して、いざ実行してみると、

あれ?

実行時例外になってしまった...

Unhandled exception. Microsoft.Data.SqlClient.SqlException (0x80131904):
Introducing FOREIGN KEY constraint 'FK_Players_Equipments_LeftEquipmentId' on table 'Players' may cause cycles or multiple cascade paths.
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.

例外の原因は Entity Framework Core とプログラマとの行き違い!?

どうやら先の実装だと、

「装備マスタ上の装備の、いずれかひとつを選んで、勇者 (プレイヤー) に関連付けする」

というよりは、

「この勇者 (プレイヤー) インスタンスが持っている装備は、この装備レコードに記載されている」

というように、生存期間も同じな 1:1 の関連付けとして判断されるようだ。

そのため、そのようなモデリングでは大事な、連鎖削除も同時に構成されるらしい。
ところがそのような前提で「装備マスタ」と「勇者 (プレイヤー)」とを関連付けようとすると、現状の「勇者 (プレイヤー)」は 2 つの「装備マスタ」参照を持っているため、連鎖削除の循環参照が発生し、解決できなくなるらしい。

ちゃんと言わなければ伝わらないことだってある

さてさて、そのような Entity Framework Core との誤解 (?) を解くには、すなわち、既定のモデリング則に当てはまらない場合は、ちゃんとプログラマが「いやいや、ここでやりたいモデリングはこうなんだよ」と明示してやる必要がある。

今回の場合だと、連鎖削除の制約は無用であることを、Entity Framework Core に教えてあげるとよい。
(実際、今回のモデリングでは、「勇者 (プレイヤー)」レコードが削除されることで、そのプレイヤーが持っていた装備も装備マスタから削除されてしまってはよろしくない訳である)

具体的には、データベースコンテキストクラスで OnModelCreating() メソッドをオーバーライドし、その中で指示してやればよい。
public class MyGameDbContext : DbContext
{
  public DbSet<Equipment> Equipments { get; set; }
  public DbSet<Player> Players { get; set;}
  ...
  protected override void OnModelCreating(ModelBuilder builder)
  {
    base.OnModelCreating(builder);
    var playerEntity = builder.Entity<Player>();
    playerEntity.HasOne(t => t.LeftEquipment).WithMany()
      .OnDelete(DeleteBehavior.NoAction);
    playerEntity.HasOne(t => t.RightEquipment).WithMany()
      .OnDelete(DeleteBehavior.NoAction);
  }
}

これで、「勇者(プレイヤー)」と「装備マスタ」との関係は、左右の手ごとに 1:N の関係であることや、連鎖削除は無用であることを明示できた。

解決!

以上でプログラム実行すると、ちゃんと期待どおりにデータベースが構築された。

また、「勇者 (プレイヤー)」レコードに対応する Player オブジェクトに対し、LeftEquipment および RightEquipment のナビゲーションプロパティを介して、持っている装備の参照や更新 (装備マスタからの装備の持ち替え) も期待どおり動作するようになった。

ちなみに、自分の知る限り、連鎖削除の有無の制御は、上記のように  Fluent API を使って行なうしかないようだ。
(つまり、属性指定では制御・指定はできないっぽい)

以上

# by developer-adjust | 2021-05-31 19:07 | .NET | Comments(0)