bucket-sort logo bucket-sort

プログラミングとインフラエンジニアリングの覚え書き

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. [C#] DbContext の設定と保存処理を拡張する

[C#] DbContext の設定と保存処理を拡張する

Aug 6, 2026 C# , .NET , Entity Framework Core bucket-sort

前回は、エンティティ、表示用モデル、入力用モデルを分ける考え方を見ました。

今回は、DbContext の中でアプリケーション共通のデータアクセス規則を扱います。

DbContext は、単に DbSet<T> を並べる場所ではありません。
テーブル名、列の制約、リレーション、変更追跡、保存処理の拡張など、データベースとの接点をまとめる中心です。

マッピング設定を分ける

OnModelCreating() にすべての設定を書くと、エンティティが増えるにつれて長くなります。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Car>(entity =>
    {
        entity.ToTable("Inventory");
        entity.Property(car => car.Make).HasMaxLength(50).IsRequired();
        entity.Property(car => car.Color).HasMaxLength(50).IsRequired();
    });

    modelBuilder.Entity<Order>(entity =>
    {
        entity.ToTable("Orders");
        entity.Property(order => order.CustomerName).HasMaxLength(80);
    });
}

エンティティごとに設定クラスへ分けると、見通しがよくなります。

using AutoLot.Models.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace AutoLot.Dal.Configuration;

public sealed class CarConfiguration : IEntityTypeConfiguration<Car>
{
    public void Configure(EntityTypeBuilder<Car> builder)
    {
        builder.ToTable("Inventory");

        builder.HasKey(car => car.Id);

        builder.Property(car => car.Make)
            .HasMaxLength(50)
            .IsRequired();

        builder.Property(car => car.Color)
            .HasMaxLength(50)
            .IsRequired();

        builder.Property(car => car.PetName)
            .HasMaxLength(50);
    }
}

DbContext 側ではまとめて適用できます。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfigurationsFromAssembly(typeof(AutoLotContext).Assembly);
}

設定が増えても、DbContext が巨大になりにくくなります。

共通の規約を上書きする

すべての string に既定の長さを設定したい場合があります。
そのような共通規則は、モデル構築時の規約として指定できます。

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<string>()
        .HaveMaxLength(100);

    configurationBuilder.Properties<decimal>()
        .HavePrecision(18, 2);
}

個別設定が必要な列は、エンティティ設定で上書きします。

builder.Property(car => car.Make)
    .HasMaxLength(50)
    .IsRequired();

全体の標準を決め、例外だけを個別に書くと、設定の意図が読みやすくなります。

変更追跡イベントを使う

EF Core は、エンティティの状態を追跡しています。
この状態変化に反応して処理を差し込むこともできます。

public sealed class AutoLotContext : DbContext
{
    public AutoLotContext(DbContextOptions<AutoLotContext> options)
        : base(options)
    {
        ChangeTracker.Tracked += OnEntityTracked;
        ChangeTracker.StateChanged += OnEntityStateChanged;
    }

    private static void OnEntityTracked(
        object? sender,
        EntityTrackedEventArgs eventArgs)
    {
        Console.WriteLine(
            $"追跡開始: {eventArgs.Entry.Entity.GetType().Name}");
    }

    private static void OnEntityStateChanged(
        object? sender,
        EntityStateChangedEventArgs eventArgs)
    {
        Console.WriteLine(
            $"{eventArgs.Entry.Entity.GetType().Name}: " +
            $"{eventArgs.OldState} -> {eventArgs.NewState}");
    }
}

イベントは便利ですが、何でも入れる場所ではありません。
ログ出力や診断には向いていますが、業務上重要な保存ルールは SaveChanges() の拡張やサービスに置いた方が明確です。

保存時に監査情報を入れる

業務アプリケーションでは、作成日時や更新日時を自動で入れたいことがよくあります。

共通インターフェイスを用意します。

public interface IAuditable
{
    DateTime CreatedAt { get; set; }
    DateTime UpdatedAt { get; set; }
}

エンティティに実装します。

public sealed class Car : IAuditable
{
    public int Id { get; set; }
    public string Make { get; set; } = "";
    public string Color { get; set; } = "";
    public string PetName { get; set; } = "";

    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
}

SaveChangesAsync() を上書きします。

public override Task<int> SaveChangesAsync(
    CancellationToken cancellationToken = default)
{
    ApplyAuditValues();
    return base.SaveChangesAsync(cancellationToken);
}

private void ApplyAuditValues()
{
    var now = DateTime.UtcNow;

    foreach (var entry in ChangeTracker.Entries<IAuditable>())
    {
        if (entry.State == EntityState.Added)
        {
            entry.Entity.CreatedAt = now;
            entry.Entity.UpdatedAt = now;
        }

        if (entry.State == EntityState.Modified)
        {
            entry.Entity.UpdatedAt = now;
        }
    }
}

呼び出し側が毎回日時を設定しなくても、保存時に一貫して入ります。

論理削除を保存処理に組み込む

物理削除ではなく、削除済みフラグを立てたい場合があります。

public interface ISoftDeletable
{
    bool IsDeleted { get; set; }
    DateTime? DeletedAt { get; set; }
}

Remove() されたエンティティを、保存前に更新扱いへ変換します。

private void ApplySoftDelete()
{
    var entries = ChangeTracker.Entries<ISoftDeletable>()
        .Where(entry => entry.State == EntityState.Deleted);

    foreach (var entry in entries)
    {
        entry.State = EntityState.Modified;
        entry.Entity.IsDeleted = true;
        entry.Entity.DeletedAt = DateTime.UtcNow;
    }
}

保存処理から呼び出します。

public override Task<int> SaveChangesAsync(
    CancellationToken cancellationToken = default)
{
    ApplyAuditValues();
    ApplySoftDelete();
    return base.SaveChangesAsync(cancellationToken);
}

さらに、通常の問い合わせで削除済みを除外するなら、グローバルフィルターを設定します。

modelBuilder.Entity<Car>()
    .HasQueryFilter(car => !car.IsDeleted);

これで、削除処理のルールを各画面に散らさずに済みます。

保存処理に入れすぎない

SaveChanges() の拡張は強力です。
ただし、何でもここに入れると、保存時に何が起きるのか見えにくくなります。

向いている処理は、次のような共通処理です。

  • 作成日時、更新日時の設定
  • 論理削除への変換
  • 変更履歴の記録
  • 共通の検証

一方で、次のような処理は慎重に扱うべきです。

  • メール送信
  • 外部 API 呼び出し
  • 時間のかかる集計
  • 画面固有の判断

保存処理は、データベースへ書き込む直前の最後の関門です。
便利な場所だからこそ、共通性の高い処理に絞ると安定します。

データアクセス層の中で完結させる

DbContext の設定や保存処理を整えると、画面側のコードは軽くなります。

public async Task<IActionResult> Delete(int id)
{
    var car = await _repository.FindAsync(id);

    if (car is null)
    {
        return NotFound();
    }

    _repository.Delete(car);
    await _repository.SaveChangesAsync();

    return RedirectToAction(nameof(Index));
}

このコードは、物理削除か論理削除かを知りません。
それはデータアクセス層の責務です。

境界をうまく作ると、呼び出し側は「何をしたいか」に集中できます。
データベースの細かい都合は、データアクセス層の中に閉じ込められます。

次回は、データアクセス層を呼び出す入口として、リポジトリの作り方を見ていきます。

C# .NET Entity Framework Core DbContext SaveChanges 監査ログ
← [C#] EF Core のエンティティと表示用モデルを設計する [C#] EF Core とリポジトリでデータ操作を整理する →

Related Posts

  • [C#] EF Core でレコードを追加する Jul 29, 2026
  • [C#] EF Core のコマンドライン操作とマイグレーションを理解する Jul 28, 2026
  • [C#] EF Core の基本部品を理解する Jul 26, 2026
  • [C#] EF Core でデータベースの準備と初期データを扱う Aug 8, 2026

Table of Contents

  • マッピング設定を分ける
  • 共通の規約を上書きする
  • 変更追跡イベントを使う
  • 保存時に監査情報を入れる
  • 論理削除を保存処理に組み込む
  • 保存処理に入れすぎない
  • データアクセス層の中で完結させる

Recent Posts

  • [C#] EF Core でデータベースの準備と初期データを扱う Aug 8, 2026
  • [C#] EF Core とリポジトリでデータ操作を整理する Aug 7, 2026
  • [C#] DbContext の設定と保存処理を拡張する Aug 6, 2026
  • [C#] EF Core のエンティティと表示用モデルを設計する Aug 5, 2026
  • [C#] EF Core を中心にデータアクセス層を分ける Aug 4, 2026

Categories

  • C#150
  • .NET149
  • AWS27
  • Laravel16
  • Entity Framework Core15
  • Linux15
  • MySQL9
  • Apache8
  • PHP8
  • Data Access6
  • DynamoDB6
  • セキュリティ6
  • Nginx5
  • WordPress4
  • インフラ4
  • Hugo3
  • .NET Framework1
  • Aurora1
  • Diagnostics1
  • Filament1

Tags

  • C#
  • .NET
  • AWS
  • Laravel
  • コレクション
  • PHP
  • Entity Framework Core
  • セキュリティ
  • MySQL
  • Linux
  • パフォーマンス
  • Apache
  • LINQ
  • System.Collections.Generic
  • デリゲート
  • リフレクション
  • ADO.NET
  • Code Snippet
  • DynamoDB
  • NoSQL
  • PHP-FPM
  • RDS
  • System.Collections
  • Windows
  • メタデータ
  • メモリ管理
  • CIL
  • DoS
  • Nginx
  • SQL Server
  • WordPress
  • ラムダ式
  • 監視
  • 設計
  • Amazon Linux 2023
  • Delegate
  • Docker
  • IDisposable
  • Ipset
  • Iptables
  • LINQ to Objects
  • OPCache
  • Pointer
  • Reflection
  • System.Collections.Specialized
  • Unsafe
  • Webサーバー
  • アセンブリ
  • インターフェース
  • オブジェクト指向
Powered by Hugo & Explore Theme.