bucket-sort logo bucket-sort

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

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. [C#] EF Core とリポジトリでデータ操作を整理する

[C#] EF Core とリポジトリでデータ操作を整理する

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

前回は、DbContext の設定と保存処理を拡張する方法を見ました。

今回は、データアクセス層の入口として リポジトリ を作ります。

リポジトリは、データの取得、追加、更新、削除をまとめるための設計です。
画面やサービスが DbContext を直接触らず、目的に合ったメソッドを通してデータへアクセスできるようにします。

リポジトリを使う目的

EF Core の DbContext 自体も、すでにリポジトリに近い役割を持っています。
そのため、必ず独自リポジトリを作るべきというわけではありません。

それでもリポジトリが役立つ場面があります。

  • よく使う検索条件を名前付きメソッドにしたい
  • 画面側から Include() や AsNoTracking() を隠したい
  • 保存時の例外処理や同時実行制御をまとめたい
  • テストでデータアクセス部分を差し替えたい
  • 複数のエンティティに共通する操作をそろえたい

大切なのは、EF Core を薄く包むだけの層を増やさないことです。
リポジトリには、アプリケーションの意図が読み取れるメソッドを置くと価値が出ます。

基本のインターフェイス

まず、共通操作を表すインターフェイスを作ります。

public interface IRepository<T>
    where T : class
{
    Task<T?> FindAsync(int id);
    Task<List<T>> GetAllAsync();
    Task AddAsync(T entity);
    void Update(T entity);
    void Delete(T entity);
    Task<int> SaveChangesAsync();
}

実装です。

using Microsoft.EntityFrameworkCore;

public class Repository<T> : IRepository<T>
    where T : class
{
    protected readonly AutoLotContext Context;
    protected readonly DbSet<T> Table;

    public Repository(AutoLotContext context)
    {
        Context = context;
        Table = context.Set<T>();
    }

    public virtual async Task<T?> FindAsync(int id)
    {
        return await Table.FindAsync(id);
    }

    public virtual async Task<List<T>> GetAllAsync()
    {
        return await Table.ToListAsync();
    }

    public virtual async Task AddAsync(T entity)
    {
        await Table.AddAsync(entity);
    }

    public virtual void Update(T entity)
    {
        Table.Update(entity);
    }

    public virtual void Delete(T entity)
    {
        Table.Remove(entity);
    }

    public Task<int> SaveChangesAsync()
    {
        return Context.SaveChangesAsync();
    }
}

この段階では、まだ薄い共通化です。
本当に大事なのは、エンティティごとの目的に合った操作を足していくところです。

読み取り専用の共通リポジトリ

一覧表示やビューからの取得のように、更新しない読み取り処理だけを分けたい場合があります。

public interface IReadOnlyRepository<T>
    where T : class
{
    Task<T?> FindAsync(int id);
    Task<List<T>> GetAllAsync();
}

読み取り専用の実装では、追跡なしを既定にできます。

public class ReadOnlyRepository<T> : IReadOnlyRepository<T>
    where T : class
{
    private readonly DbSet<T> _table;

    public ReadOnlyRepository(AutoLotContext context)
    {
        _table = context.Set<T>();
    }

    public async Task<T?> FindAsync(int id)
    {
        return await _table.FindAsync(id);
    }

    public async Task<List<T>> GetAllAsync()
    {
        return await _table
            .AsNoTracking()
            .ToListAsync();
    }
}

ただし、FindAsync() はコンテキストの追跡状態も見に行きます。
完全に追跡なしで統一したい場合は、主キー条件を明示したメソッドにするなど、エンティティごとに設計する方がよいです。

車専用のリポジトリ

共通リポジトリだけでは、アプリケーション固有の意図が出ません。
車専用のインターフェイスを作ります。

public interface ICarRepository : IRepository<Car>
{
    Task<List<CarListItem>> GetAvailableCarsAsync();
    Task<CarDetail?> GetDetailAsync(int id);
    Task<bool> ExistsAsync(int id);
}

実装です。

public sealed class CarRepository : Repository<Car>, ICarRepository
{
    public CarRepository(AutoLotContext context)
        : base(context)
    {
    }

    public async Task<List<CarListItem>> GetAvailableCarsAsync()
    {
        return await Context.Cars
            .AsNoTracking()
            .Where(car => car.IsDrivable)
            .OrderBy(car => car.Make)
            .ThenBy(car => car.PetName)
            .Select(car => new CarListItem
            {
                Id = car.Id,
                DisplayName = car.Make + " " + car.Color + " " + car.PetName,
                Status = "販売可能",
                OrderCount = car.Orders.Count
            })
            .ToListAsync();
    }

    public async Task<CarDetail?> GetDetailAsync(int id)
    {
        return await Context.Cars
            .AsNoTracking()
            .Where(car => car.Id == id)
            .Select(car => new CarDetail
            {
                Id = car.Id,
                Make = car.Make,
                Color = car.Color,
                PetName = car.PetName,
                IsDrivable = car.IsDrivable,
                DateBuilt = car.DateBuilt,
                Orders = car.Orders
                    .OrderByDescending(order => order.OrderDate)
                    .Select(order => new OrderSummary
                    {
                        Id = order.Id,
                        CustomerName = order.CustomerName,
                        OrderDate = order.OrderDate
                    })
                    .ToList()
            })
            .SingleOrDefaultAsync();
    }

    public async Task<bool> ExistsAsync(int id)
    {
        return await Context.Cars.AnyAsync(car => car.Id == id);
    }
}

GetAvailableCarsAsync() という名前を見るだけで、何を取りたいのかがわかります。
呼び出し側は、Where() や Select() の詳細を知る必要がありません。

依存性注入へ登録する

ASP.NET Core などで使う場合は、リポジトリをサービスとして登録します。

builder.Services.AddDbContext<AutoLotContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("AutoLot"));
});

builder.Services.AddScoped<ICarRepository, CarRepository>();

画面側ではインターフェイスに依存します。

public sealed class CarsController : Controller
{
    private readonly ICarRepository _cars;

    public CarsController(ICarRepository cars)
    {
        _cars = cars;
    }

    public async Task<IActionResult> Index()
    {
        var cars = await _cars.GetAvailableCarsAsync();
        return View(cars);
    }
}

この形にすると、画面側は DbContext の具体的な使い方から少し離れられます。

保存単位をどう扱うか

共通リポジトリに SaveChangesAsync() を持たせると簡単です。
ただし、複数のリポジトリをまたいで一度に保存したい場合は、保存単位を別に考える必要があります。

public interface IUnitOfWork
{
    ICarRepository Cars { get; }
    IOrderRepository Orders { get; }
    Task<int> SaveChangesAsync();
}
public sealed class UnitOfWork : IUnitOfWork
{
    private readonly AutoLotContext _context;

    public UnitOfWork(
        AutoLotContext context,
        ICarRepository cars,
        IOrderRepository orders)
    {
        _context = context;
        Cars = cars;
        Orders = orders;
    }

    public ICarRepository Cars { get; }
    public IOrderRepository Orders { get; }

    public Task<int> SaveChangesAsync()
    {
        return _context.SaveChangesAsync();
    }
}

この考え方を使うと、複数の操作をひとつの保存単位として扱いやすくなります。

await unitOfWork.Cars.AddAsync(car);
await unitOfWork.Orders.AddAsync(order);
await unitOfWork.SaveChangesAsync();

ただし、EF Core の DbContext 自体も作業単位を表しています。
独自の作業単位を追加するかどうかは、アプリケーションの規模と複雑さを見て判断します。

リポジトリの使いすぎに注意する

リポジトリは便利ですが、すべての LINQ を隠す必要はありません。

次のようなメソッドが増えすぎると、かえって保守しづらくなります。

Task<List<Car>> GetRedCarsAsync();
Task<List<Car>> GetBlueCarsAsync();
Task<List<Car>> GetRedToyotaCarsAsync();
Task<List<Car>> GetBlueToyotaCarsAsync();

条件の組み合わせが増えるなら、検索条件をオブジェクトとして渡す方がよい場合があります。

public sealed class CarSearchCondition
{
    public string? Make { get; set; }
    public string? Color { get; set; }
    public bool? IsDrivable { get; set; }
}
public async Task<List<CarListItem>> SearchAsync(CarSearchCondition condition)
{
    var query = Context.Cars.AsNoTracking().AsQueryable();

    if (!string.IsNullOrWhiteSpace(condition.Make))
    {
        query = query.Where(car => car.Make == condition.Make);
    }

    if (!string.IsNullOrWhiteSpace(condition.Color))
    {
        query = query.Where(car => car.Color == condition.Color);
    }

    if (condition.IsDrivable is not null)
    {
        query = query.Where(car => car.IsDrivable == condition.IsDrivable);
    }

    return await query
        .OrderBy(car => car.Make)
        .Select(car => new CarListItem
        {
            Id = car.Id,
            DisplayName = car.Make + " " + car.Color + " " + car.PetName,
            Status = car.IsDrivable ? "販売可能" : "整備中",
            OrderCount = car.Orders.Count
        })
        .ToListAsync();
}

名前付きメソッドと検索条件オブジェクトを使い分けると、リポジトリが膨らみすぎるのを防げます。

この記事のまとめ

リポジトリは、データアクセスの入口をそろえるための設計です。
ただし、EF Core を機械的に包むだけでは価値が薄くなります。

重要なのは、アプリケーションの意図を名前にすることです。

GetAvailableCarsAsync()、GetDetailAsync()、SearchAsync() のようなメソッドは、呼び出し側に「何をしたいのか」を伝えます。
その内側で、Include()、投影、追跡なし、同時実行制御などを適切に扱うのがデータアクセス層の役割です。

次回は、マイグレーションやデータベース初期化をプログラムから扱う方法を見ていきます。

C# .NET Entity Framework Core Repository データアクセス層
← [C#] DbContext の設定と保存処理を拡張する [C#] EF Core でデータベースの準備と初期データを扱う →

Related Posts

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