EF Core のマイグレーション内で appsettings.json の値(IConfiguration)を使いたいのに、Migration クラスは引数なしコンストラクタ前提で DI が効かず詰まることがあります。Azure SQL Database の Temporal Table で HISTORY_RETENTION_PERIOD を環境ごとに変えたいケースを例に、実装手順と運用上の落とし穴を整理します。
結論:Migration から IConfiguration を読むには「起動時にどこかへ置く」が現実的
EF Core のマイグレーションクラス(Migration を継承したクラス)は、基本的に引数なしコンストラクタで生成されます。つまり、普段の ASP.NET Core のようにコンストラクタ DI で IConfiguration や IOptions<T> を受け取る、という書き方ができません。
そこで現実的な解としては、アプリ起動時に IConfiguration を一度だけ取得し、マイグレーション側から参照できる場所(静的プロパティなど)に退避しておく方法です。今回の記事では、最もシンプルで実装量が少ない「静的クラス + 静的プロパティ」パターンを中心に、実運用で困りやすいポイントまで含めて解説します。
背景:HISTORY_RETENTION_PERIOD を appsettings から制御したい理由
Azure SQL Database や SQL Server のシステムバージョン管理テーブル(Temporal Table)を使うと、更新履歴がヒストリーテーブルに自動で蓄積されます。このとき、履歴をどれだけ保持するか(保持期間)を DB 側の設定として持てるのが HISTORY_RETENTION_PERIOD です。
運用を考えると、保持期間は「どの環境か」によって変えたくなることがよくあります。
- 開発環境:デバッグしやすいように長めに保持したい
- ステージング環境:本番に近いがコストを抑えたい
- 本番環境:監査要件・問い合わせ対応・コストのバランスで決めたい
この「環境ごとに違う値」を、appsettings.json(や環境変数、Key Vault など)から引いて設定できると便利です。スキーマ変更は EF Core マイグレーションで一元管理しているなら、保持期間の設定もマイグレーションに含めたくなります。
なぜマイグレーションクラスでは DI が使いにくいのか
マイグレーションは「アプリのリクエスト処理」ではなく「DB の変更履歴を表すクラス」です。EF Core はこのクラスを内部で生成して実行しますが、そこで ASP.NET Core の DI コンテナが素直に介入できない設計になっています。
| やりたいこと | 普段の ASP.NET Core なら | Migration では |
|---|---|---|
| 設定値を読む | コンストラクタで IConfiguration / IOptions を注入 | Migration は引数なし生成が前提のため注入できない |
| 環境判定をする | IHostEnvironment を注入して判定 | 同様に注入できないため、別の経路が必要 |
| サービスを呼ぶ | 任意のサービスを DI で受け取る | Migration 内で複雑な依存関係を持たせるのは難しい |
そのため、Migration は「基本的に純粋なスキーマ変更」に寄せるのが王道です。ただし、どうしても DB オプション設定(保持期間など)をマイグレーションでやりたい場合は、アプリ設定を取得する経路を工夫する必要があります。
解決策:静的な GlobalConfiguration に IConfiguration を置く
方法はシンプルです。起動時に IConfiguration を静的プロパティへセットしておき、Migration 側はその静的プロパティから参照します。
GlobalConfiguration クラスを作る
using Microsoft.Extensions.Configuration;
public static class GlobalConfiguration
{
// アプリ起動時に 1 回だけセットし、読み取り専用で使う想定
public static IConfiguration? Configuration { get; set; }
}
「静的プロパティ」は乱用するとテストや依存関係の見通しが悪くなりますが、Migration のように DI が入りにくい場所では、最小限の用途に限定することで割り切りやすい選択肢になります。
Program.cs(最小ホスティング)でセットする
var builder = WebApplication.CreateBuilder(args);
// ここまでの時点で builder.Configuration は IConfiguration として利用可能
GlobalConfiguration.Configuration = builder.Configuration;
var app = builder.Build();
app.MapControllers();
app.Run();
ポイントは「マイグレーションが走る前」にセットすることです。たとえばアプリ起動時に context.Database.Migrate() を実行している場合、その呼び出しより前に GlobalConfiguration.Configuration をセットしておく必要があります。
Startup.cs パターンでも同じ
.NET 5 以前や Startup.cs 形式の場合も、早い段階でセットすれば同様に動きます。
public class Startup
{
public Startup(IConfiguration configuration)
{
GlobalConfiguration.Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
}
}
実装例:appsettings で保持期間を定義し、Migration で SQL を流す
保持期間の値は appsettings に置いておき、Migration 内で読み出して SQL を実行します。例として「日数で保持期間を決める」ケースを扱います。
appsettings.json の例
{
"DatabaseSettings": {
"HistoryRetentionDays": 30
}
}
Migration で値を読み、SQL を発行する
Migration 内では GlobalConfiguration.Configuration から値を取得します。数値は必ずバリデーションしてから SQL に埋め込みます(設定値が誤っていた場合でも DB を壊さないため)。
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Configuration;
public partial class SetHistoryRetentionPeriod : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// SQL Server / Azure SQL 前提の SQL なので、念のためプロバイダを確認
if (ActiveProvider != "Microsoft.EntityFrameworkCore.SqlServer")
{
return;
}
var config = GlobalConfiguration.Configuration;
if (config is null)
{
// ここで黙ってスキップすると後で原因不明になりやすいので、
// 失敗させて気付けるようにするのがおすすめ
throw new InvalidOperationException(
"GlobalConfiguration.Configuration が未設定です。Program.cs で設定してからマイグレーションを実行してください。");
}
// 設定値を読む(なければ null)
var retentionDays = config.GetValue<int?>("DatabaseSettings:HistoryRetentionDays");
// 値の妥当性チェック(運用ルールに合わせて上限・下限を調整)
if (retentionDays is null)
{
// 未設定なら何もしない、または既定値を適用する、など方針を決める
return;
}
var days = retentionDays.Value;
// 例:1〜3650日の範囲に制限(約10年)
if (days < 1 || days > 3650)
{
throw new InvalidOperationException(
$"HistoryRetentionDays の値が不正です: {days}(許容範囲: 1〜3650)");
}
// Temporal Table の保持期間は、通常「システムバージョン管理が有効なテーブル」に設定します。
// 既に SYSTEM_VERSIONING = ON の場合、保持期間だけを変更できる構文を使います。
migrationBuilder.Sql($@"
ALTER TABLE dbo.YourTemporalTable
SET (SYSTEM_VERSIONING = ON (HISTORY_RETENTION_PERIOD = {days} DAYS));
");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Down は「元に戻す」必要があるかを検討
// 例:保持期間を無効化したい場合は HISTORY_RETENTION_PERIOD = INFINITE などを設定(要件に合わせる)
}
}
上の SQL は一例です。実際には、対象テーブル名、既に SYSTEM_VERSIONING が有効か、履歴テーブル名を明示しているか、などによって最適な SQL が変わります。重要なのは「マイグレーションから設定値を取り出し、DB へ適用できるようにする」という枠組みです。
SQL を直接書くときの最低限のチェックポイント
| チェック項目 | 見落とすとどうなるか | 対策 |
|---|---|---|
| 対象が Temporal Table(SYSTEM_VERSIONING)か | ALTER が失敗してマイグレーション全体がロールバックする | 対象テーブルの前提をコメントに残す/事前に検証する |
| SQL Server 用の SQL を他のプロバイダで実行しないか | SQLite などでテスト時に落ちる | ActiveProvider で分岐する |
| 設定値の型・範囲 | 誤設定で極端な保持期間になり、コストや容量を圧迫 | 上下限を設けて例外で止める |
| SQL インジェクション | 設定値が文字列の場合、意図しない SQL が混入する可能性 | 数値化してから埋め込み、文字列は使わない |
dotnet ef で実行するときの落とし穴:Program.cs が走らない場合がある
ここが実運用で最もハマりやすい点です。マイグレーションは「アプリ起動時に自動適用」することもあれば、開発者が dotnet ef database update で適用することもあります。
後者(CLI 実行)の場合、あなたのプロジェクト構成によっては Program.cs が意図通り実行されず、GlobalConfiguration.Configuration が未設定のまま Migration が走ることがあります。そうすると、上の例のように例外で止まります。
対策:IDesignTimeDbContextFactory で設定を組み立て、GlobalConfiguration にもセットする
EF Core ツールが DbContext を生成するための入口として IDesignTimeDbContextFactory<TContext> を用意すると、CLI 実行時にも「どの設定ファイルを読むか」を制御できます。ここで GlobalConfiguration.Configuration もセットすれば、Migration 側からも参照できます。
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] args)
{
var environmentName =
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
.AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: false)
.AddEnvironmentVariables()
.Build();
// 重要:デザイン時でも Migration から見えるようにする
GlobalConfiguration.Configuration = configuration;
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection"));
return new AppDbContext(optionsBuilder.Options);
}
}
このクラスを追加しておくと、ローカルで dotnet ef を使う開発スタイルでも「設定値が null で落ちる」問題を避けやすくなります。
| マイグレーションの実行方法 | 設定が入るタイミング | GlobalConfiguration のセット場所 |
|---|---|---|
アプリ起動時に Database.Migrate() | Program.cs が必ず走る | Program.cs / Startup.cs |
dotnet ef database update | Program.cs が走らない/条件次第で走る | IDesignTimeDbContextFactory を用意して確実にセット |
運用上の注意:マイグレーションは「再現性」が命
ここまでの方法で「Migration から appsettings の値を読む」こと自体はできます。ただし、マイグレーションがその時点の設定値に依存するようになると、将来の運用で困る可能性があります。
- 同じマイグレーションでも、環境によって適用結果が変わる
- appsettings を変更しただけで、古いマイグレーションの意味が変わる
- 障害調査時に「当時どの値で適用されたか」が追いづらい
とはいえ、保持期間のように「環境によって変えるのが自然」な設定もあります。大切なのは、再現性を壊しにくい形で運用ルールを決めることです。
再現性と環境差分を両立させる考え方
| 方針 | 向いているケース | 具体策 |
|---|---|---|
| マイグレーションは「構造」だけに寄せる | 設定値が頻繁に変わる/監査が厳しい | 保持期間は別スクリプト・運用手順で適用する |
| 環境依存でもマイグレーションでやる | 運用の自動化を優先/環境差分が許容される | appsettings で読み、範囲チェック+ログで可視化 |
| 「当時の値」を残す | 後から追跡できるようにしたい | 適用した値を監査テーブルに書き込む(SQL で INSERT) |
もう一歩実践的に:適用した保持期間を監査テーブルへ残す
「当時どの保持期間で適用されたのか」を追えるようにしたい場合、マイグレーション内で監査用テーブルへ値を記録しておくと、後々の調査が楽になります。
例として、次のようなテーブルを用意しておきます(すでに監査基盤があるならそちらに合わせてください)。
CREATE TABLE dbo.SchemaOperationLog
(
Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
OperationName NVARCHAR(200) NOT NULL,
AppliedValue NVARCHAR(200) NULL,
AppliedAtUtc DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
);
そして Migration で保持期間を適用した直後に INSERT します。
migrationBuilder.Sql($@"
INSERT INTO dbo.SchemaOperationLog (OperationName, AppliedValue)
VALUES (N'HISTORY_RETENTION_PERIOD', N'{days} DAYS');
");
この方法なら、appsettings を後から変えても「当時の適用値」が DB に残りやすくなります。監査テーブルの有無はチームの運用成熟度にもよりますが、環境依存の値をマイグレーションに入れるなら検討する価値があります。
代替案:環境依存の値はマイグレーションから切り離す
保持期間のような「運用パラメータ」は、必ずしもマイグレーションに入れる必要はありません。むしろ、長期運用を想定すると「スキーマ変更」と「運用設定」を分離したほうが安全な場合もあります。
| 方法 | メリット | デメリット | おすすめ度 |
|---|---|---|---|
| マイグレーションで設定する(本記事の方法) | 適用が自動化され、環境構築が一発で済む | 再現性が設定ファイルに左右される | 環境差分が許容されるなら有力 |
| デプロイ後スクリプトで設定する | 設定の変更履歴を運用側で管理しやすい | 適用漏れのリスク、手順が増える | 監査要件が強いなら有力 |
| 管理ツール(運用 API / 管理画面)で設定する | 環境ごとの調整がしやすく、変更も即時反映できる | ツール側の実装・権限設計が必要 | 大規模運用で有力 |
「マイグレーションでやるべきか?」の判断に迷ったら、まずはスキーマ変更を確実に反映できることを優先し、運用設定は段階的に自動化していくのが現実的です。
よくある質問と実務的な回答
GlobalConfiguration を使うのはアンチパターンでは?
一般論としては、静的なグローバル状態は依存関係を見えにくくし、テストもしづらくします。ただし、Migration のように「そもそも DI が届きにくい場所」に限定し、用途を IConfiguration の読み取り程度に絞るなら、実務上は十分に許容できるケースが多いです。
IOptions を使いたい場合は?
Migration で IOptions を直接注入するのは難しいため、基本は IConfiguration から必要な値だけを読むのがシンプルです。どうしても強い型付けが欲しい場合は、Migration 内で最小限だけバインドする方法があります。
public sealed class DatabaseSettings
{
public int HistoryRetentionDays { get; set; }
}
// Migration 内
var settings = config.GetSection("DatabaseSettings").Get();
var days = settings?.HistoryRetentionDays ?? 0;
ただしこの場合、Microsoft.Extensions.Configuration.Binder が参照できること(パッケージ参照/using)が前提になります。
設定値が変わったら、過去のマイグレーションも変わってしまわない?
変わります。だからこそ、環境依存の値を Migration に入れる場合は「値を変えたら新しいマイグレーションで上書きする」運用に寄せるのが安全です。たとえば保持期間を 30→60 に変えるなら、appsettings だけ変えるのではなく、保持期間を更新する専用のマイグレーションを追加します。
実装チェックリスト
- Program.cs / Startup.cs で
GlobalConfiguration.Configurationを確実にセットしている - CLI 実行(dotnet ef)も使うなら
IDesignTimeDbContextFactoryでもセットしている - Migration 内で
ActiveProviderを確認し、プロバイダ依存 SQL を誤実行しない - 設定値は数値化し、上限・下限を設けて不正値で止まるようにする
- 環境依存の値を Migration に入れる理由と、変更時の運用ルールをチームで共有する
まとめ
EF Core のマイグレーションではコンストラクタ DI が使いにくいため、IConfiguration を参照したいときは「起動時に静的プロパティへセットし、Migration から参照する」方法が手早く効きます。加えて、dotnet ef を使う開発フローなら IDesignTimeDbContextFactory でも同じ設定を組み立てて渡すと安定します。
一方で、マイグレーションは再現性が重要です。保持期間のように環境差分が入りやすい値を扱うときは、バリデーション・監査ログ・変更時の運用ルールまで含めて設計すると、後から困りにくい構成になります。

コメント