bucket-sort logo bucket-sort

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

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. [C#] データアクセス層の例外とモデルを仕上げる

[C#] データアクセス層の例外とモデルを仕上げる

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

前回は、既存データベースから EF Core のモデルを生成する流れを見ました。

今回は、生成したモデルや手書きしたモデルを、アプリケーションで使いやすい形へ仕上げていきます。
特に、データアクセス層の例外と、エンティティ、表示用モデルの分け方を扱います。

データアクセス層は、単に SQL を隠す場所ではありません。
データベースの事情を、アプリケーションの言葉へ翻訳する場所でもあります。

データベース例外をそのまま外へ出さない

EF Core で保存に失敗すると、DbUpdateException や DbUpdateConcurrencyException が発生します。
SQL Server 固有の例外が内側に含まれることもあります。

try
{
    await context.SaveChangesAsync();
}
catch (DbUpdateException ex)
{
    Console.WriteLine(ex.Message);
}

このまま画面や API へ返すと、利用者には意味がわかりにくく、内部情報を漏らす可能性もあります。

そこで、データアクセス層の中で独自例外へ変換します。

public abstract class DataAccessException : Exception
{
    protected DataAccessException(string message, Exception? innerException = null)
        : base(message, innerException)
    {
    }
}
public sealed class DuplicateRecordException : DataAccessException
{
    public DuplicateRecordException(string message, Exception? innerException = null)
        : base(message, innerException)
    {
    }
}
public sealed class RecordChangedException : DataAccessException
{
    public RecordChangedException(string message, Exception? innerException = null)
        : base(message, innerException)
    {
    }
}

呼び出し側は、データベース固有の事情ではなく、アプリケーションとして意味のある例外を扱えます。

保存例外を変換する

リポジトリやデータアクセスサービスで、保存処理を包みます。

public async Task<int> SaveChangesAsync()
{
    try
    {
        return await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException ex)
    {
        throw new RecordChangedException(
            "他の利用者によってデータが変更されています。", ex);
    }
    catch (DbUpdateException ex)
    {
        throw new DataStoreException(
            "データベースの更新に失敗しました。", ex);
    }
}

DataStoreException も用意しておきます。

public sealed class DataStoreException : DataAccessException
{
    public DataStoreException(string message, Exception? innerException = null)
        : base(message, innerException)
    {
    }
}

例外を変換するときは、元の例外を innerException として残します。
ログでは詳細を追えるようにしつつ、画面や API には整理されたメッセージを返せます。

存在しないデータをどう扱うか

指定された主キーのデータが見つからない場合、null を返す方法と、例外を投げる方法があります。

public async Task<Car?> FindAsync(int id)
{
    return await _context.Cars.FindAsync(id);
}

これは自然な形です。
ただし、後続処理では存在することが前提なら、専用メソッドを用意してもよいです。

public async Task<Car> GetRequiredAsync(int id)
{
    var car = await _context.Cars.FindAsync(id);

    return car ?? throw new RecordNotFoundException(
        $"指定された車が見つかりません。Id: {id}");
}

例外です。

public sealed class RecordNotFoundException : DataAccessException
{
    public RecordNotFoundException(string message)
        : base(message)
    {
    }
}

FindAsync() と GetRequiredAsync() のように、メソッド名で違いを表すと呼び出し側が読みやすくなります。

エンティティに必要な制約を持たせる

エンティティは、データベースへ保存される型です。
そのため、最低限の制約や状態変更のメソッドを持たせると、データの意味が明確になります。

public sealed class Car
{
    public int Id { get; private set; }
    public string Make { get; private set; } = "";
    public string Color { get; private set; } = "";
    public string PetName { get; private set; } = "";
    public bool IsDrivable { get; private set; } = true;

    private Car()
    {
    }

    public Car(string make, string color, string petName)
    {
        Rename(make, color, petName);
    }

    public void Rename(string make, string color, string petName)
    {
        if (string.IsNullOrWhiteSpace(make))
        {
            throw new ArgumentException("メーカー名は必須です。", nameof(make));
        }

        Make = make.Trim();
        Color = color.Trim();
        PetName = petName.Trim();
    }

    public void MarkAsUnavailable()
    {
        IsDrivable = false;
    }

    public void MarkAsAvailable()
    {
        IsDrivable = true;
    }
}

EF Core のために、引数なしコンストラクターを残しています。
一方で、通常の作成では必要な値を渡すコンストラクターを使う形にできます。

表示用モデルは読み取りに寄せる

画面に返す型は、保存用のエンティティと分けます。

public sealed class CarListItem
{
    public int Id { get; init; }
    public string Name { get; init; } = "";
    public string State { get; init; } = "";
}

init を使うと、作成後に値を書き換えにくくなります。
表示用モデルは、読み取り結果として扱うことが多いため、この形がよく合います。

投影処理です。

public async Task<List<CarListItem>> GetListAsync()
{
    return await _context.Cars
        .AsNoTracking()
        .OrderBy(car => car.Make)
        .Select(car => new CarListItem
        {
            Id = car.Id,
            Name = car.Make + " " + car.Color + " " + car.PetName,
            State = car.IsDrivable ? "販売可能" : "整備中"
        })
        .ToListAsync();
}

画面に必要な形をここで作ることで、画面側はデータベース構造を意識しなくて済みます。

入力用モデルは書き込みに寄せる

入力用モデルでは、利用者から受け取ってよい項目だけを定義します。

public sealed class CreateCarRequest
{
    public string Make { get; init; } = "";
    public string Color { get; init; } = "";
    public string PetName { get; init; } = "";
}

登録処理では、入力用モデルからエンティティを作ります。

public async Task<int> CreateAsync(CreateCarRequest request)
{
    var car = new Car(
        request.Make,
        request.Color,
        request.PetName);

    _context.Cars.Add(car);
    await SaveChangesAsync();

    return car.Id;
}

入力用モデルに Id や CreatedAt を含めなければ、利用者が管理項目を書き換える余地を減らせます。

モデルの仕上げは境界をはっきりさせる作業

データアクセス層のモデルを整えるとき、意識したい境界は次の 3 つです。

  • データベースに保存する型
  • 利用者へ見せる型
  • 利用者から受け取る型

この 3 つを無理にひとつのクラスへまとめると、最初は楽でも、後で変更しづらくなります。

例外も同じです。
データベースの例外をそのまま外へ出すのではなく、アプリケーションの言葉へ変換することで、呼び出し側の処理が安定します。

次回は、DbContext に集約できる共通処理をさらに深く見ていきます。

C# .NET Entity Framework Core 例外処理 エンティティ ViewModel
← [C#] EF Core で既存データベースからモデルを作る [C#] DbContext に共通ルールを集約する →

Related Posts

  • [C#] EF Core のエンティティと表示用モデルを設計する Aug 5, 2026
  • [C#] EF Core の基本部品を理解する Jul 26, 2026
  • [C#] EF Core の更新と削除をテストする Aug 17, 2026
  • [C#] EF Core の追加処理をテストする Aug 16, 2026

Table of Contents

  • データベース例外をそのまま外へ出さない
  • 保存例外を変換する
  • 存在しないデータをどう扱うか
  • エンティティに必要な制約を持たせる
  • 表示用モデルは読み取りに寄せる
  • 入力用モデルは書き込みに寄せる
  • モデルの仕上げは境界をはっきりさせる作業

Recent Posts

  • [WPF] XAML の構文を理解する: 名前空間とキーワード Aug 23, 2026
  • [WPF] Window クラスを支える基底クラス Aug 22, 2026
  • [WPF] Window クラスの役割と上位クラス Aug 21, 2026
  • [WPF] Application クラスの役割 Aug 20, 2026
  • [WPF] WPF の主要アセンブリと名前空間 Aug 19, 2026

Categories

  • C#165
  • .NET164
  • AWS27
  • Entity Framework Core24
  • Laravel16
  • Linux15
  • MySQL9
  • Apache8
  • PHP8
  • Data Access6
  • DynamoDB6
  • セキュリティ6
  • Nginx5
  • WordPress4
  • インフラ4
  • テスト4
  • Hugo3
  • .NET Framework1
  • Aurora1
  • Diagnostics1

Tags

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