EF Core マイグレーションで IConfiguration を参照して appsettings の値を使う方法(ASP.NET Core / Azure SQL)

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 updateProgram.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 でも同じ設定を組み立てて渡すと安定します。

一方で、マイグレーションは再現性が重要です。保持期間のように環境差分が入りやすい値を扱うときは、バリデーション・監査ログ・変更時の運用ルールまで含めて設計すると、後から困りにくい構成になります。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次