💻

【C#】Entity Framework Core(EF Core)について

に公開

はじめに

本記事では、C# の ORM(オブジェクトリレーショナルマッピング)フレームワークである Entity Framework Core(EF Core)について解説します。

本記事の対象

  • C#を用いてこれから開発をしていく方
  • EF Coreについて知らない方

概要

EF Core は、.NET アプリケーションとリレーショナルデータベース間のデータアクセスを容易にし、可読性/保守性の高いコード記述を実現します。
具体的には以下のようなことが可能になります。

  • LINQ を利用した直感的なSQLクエリ作成
  • マイグレーションを用いた、データベース作成(コードファースト)
  • スキャフォールドを用いた、データベースエンティティ作成(データベースファースト)

前提環境

  • .net 8.0を想定
  • 対象データベースはPostgreSQL(ver 16.2)
  • 既にプロジェクト作成済みで、以下のNuGetパッケージがインストールされていること
ライブラリ名 バージョン 説明
Microsoft.EntityFrameworkCore.Tools 8.0.13 Entity Framework Coreのコマンドラインツールを提供し、実際のデータベース操作を行うために李良い
Microsoft.EntityFrameworkCore.Design 8.0.13 Entity Framework Coreにおけるマイグレーションを行うために利用
Npgsql.EntityFrameworkCore.PostgreSQL 8.0.11 Entity Framework Coreにおける、PostgreSQLデータベース用のプロバイダーとして利用

マイグレーション

マイグレーションは、データベーススキーマの変更をコードベースで管理するための仕組みです。EF Core では、以下の手順でマイグレーションを実現できます。
いわゆる、コードファーストの考え方です。

前提

  • 接続先データベースにProductsテーブルが存在しないこと

手順

1. エンティティを定義

  • Idは主キーかつ最大文字数は10文字とする。
  • Nameは必須項目かつ最大文字数は100文字とする。
Product.cs
public class Product{
    [Key] // 主キーの指定
    [MaxLength(10)] // 最大10文字制限
    public string Id { get; set; }

    [Required] // 必須制約
    [MaxLength(100)] // 最大100文字制限
    public string Name { get; set; }
}

2. DbContextクラスを定義

ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext : DbContext
{
    // DbSetによりProductテーブルが定義される
    public DbSet<Product> Products { get; set; }

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}

3. 接続先情報を設定ファイルに記述

appsettings.json
{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    },
    "AllowedHosts": "*",
    //PostgreSQLへの接続先情報
    "ConnectionStrings": {
        "DefaultConnection": "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=postgres"
    }
}

4. エントリーポイントでDbContextクラスのインスタンスを作成

Program.cs
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// AddDbContextクラスを作成(appsettings.jsonで定義した接続先情報を用いて)
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")!));

// その他のサービス定義

var app = builder.Build();

// ミドルウェア定義

app.Run();

5. 初期マイグレーションを作成

プロジェクトのルートディレクトリで以下のコマンドを実行して初期マイグレーションを作成します。

dotnet ef migrations add InitialCreate

6. データベースにマイグレーションを適用(データベースを更新)

作成したマイグレーションをデータベースに反映させるには、以下のコマンドを実行します。
これにより、以下のように変更履歴およびテーブルが作成されます。

dotnet ef database update

7. 成果物確認

マイグレーションにより、変更履歴のコード及びテーブルが作成されます。ロールバック処理やデータベース変更履歴のGit管理が可能となります。

{実施日付}_InitialCreate.cs(自動生成)
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace EFCoreTest.Migrations
{
    /// <inheritdoc />
    public partial class InitialCreate : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Products",
                columns: table => new
                {
                    Id = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
                    Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Products", x => x.Id);
                });
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Products");
        }
    }
}

ApplicationDbContextModelSnapshot.cs(自動生成)
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;

#nullable disable

namespace EFCoreTest.Migrations
{
    [DbContext(typeof(ApplicationDbContext))]
    partial class ApplicationDbContextModelSnapshot : ModelSnapshot
    {
        protected override void BuildModel(ModelBuilder modelBuilder)
        {
#pragma warning disable 612, 618
            modelBuilder
                .HasAnnotation("ProductVersion", "8.0.13")
                .HasAnnotation("Relational:MaxIdentifierLength", 63);

            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

            modelBuilder.Entity("Product", b =>
                {
                    b.Property<string>("Id")
                        .HasMaxLength(10)
                        .HasColumnType("character varying(10)");

                    b.Property<string>("Name")
                        .IsRequired()
                        .HasMaxLength(100)
                        .HasColumnType("character varying(100)");

                    b.HasKey("Id");

                    b.ToTable("Products");
                });
#pragma warning restore 612, 618
        }
    }
}



実際に作成されたProductsテーブル


実際に作成された__EFMigrationsHistoryテーブル

スキャフォールド

スキャフォールドでは、既存のデータベースから自動的にエンティティクラスDbContextクラスを生成することができます。EF Core では、以下の手順でマイグレーションを実現できます。
いわゆる、データベースファーストの考え方です。業務要件としては、こちらを利用するパターンが多いのではないかと思います。

前提

  • 接続先データベースに以下のDDLで作成されたProductsテーブルが存在すること
create_table.sql
CREATE TABLE "Products" (
          "Id" character varying(10) NOT NULL,
          "Name" character varying(100) NOT NULL,
          CONSTRAINT "PK_Products" PRIMARY KEY ("Id")
      );

手順

1. スキャフォールドの実行

プロジェクトのルートディレクトリで以下のコマンドを実行してスキャフォールドを実行します。

dotnet ef dbcontext scaffold "Host=localhost;Database=postgres;Username=postgres;Password=postgres" Npgsql.EntityFrameworkCore.PostgreSQL --data-annotations --context-dir DBContext/  --output-dir Entities/ --force -v

本コマンドで使用しているコマンドオプションの説明は以下の通りです。

オプション 説明
Host データベースへの接続文字列。ここでは、PostgreSQLデータベースへの接続情報を指定しています。
Npgsql.EntityFrameworkCore.PostgreSQL 使用するプロバイダーを指定します。この場合は、PostgreSQL用のEntity Framework Coreプロバイダーです。
--data-annotations データアノテーションを使用してモデルを生成します。これにより、データ制約や属性がエンティティクラスに追加されます。
--context-dir DbContextクラスを生成するディレクトリを指定します。この場合、DBContext/に生成されます。
--output-dir エンティティクラスを生成するディレクトリを指定します。この場合、Entities/にエンティティが生成されます。
--force 既存のファイルを上書きするオプションです。これにより、同名のファイルが存在しても強制的に生成します。
-v 詳細な出力を表示するオプションです。コマンドの実行中に発生した情報やエラーの詳細を表示します。

2. 成果物確認

スキャフォールドを実行することで、以下のようにデータベースからDBContextクラスとEntityクラスが自動生成されることが確認できます。
既にデータベース環境がある場合はスキャフォールドを用いることで、EF Coreを利用する環境が迅速に構築できるので非常に便利な機能かと思います。

Entities/Product.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace EFCoreTest.Entities;

public partial class Product
{
    [Key]
    [StringLength(10)]
    public string Id { get; set; } = null!;

    [StringLength(100)]
    public string Name { get; set; } = null!;
}
DBContext/PostgresContext
using System;
using System.Collections.Generic;
using EFCoreTest.Entities;
using Microsoft.EntityFrameworkCore;

namespace EFCoreTest.DBContext;

public partial class PostgresContext : DbContext
{
    public PostgresContext()
    {
    }

    public PostgresContext(DbContextOptions<PostgresContext> options)
        : base(options)
    {
    }

    public virtual DbSet<Product> Products { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
        => optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=postgres");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasPostgresExtension("pg_catalog", "adminpack");

        OnModelCreatingPartial(modelBuilder);
    }

    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

Ef Coreを用いたCRUD操作

前提

以下のように、エンティティおよびContextクラスを作成しておきます。

1. Productエンティティの定義

Product.cs
public class Product
{
    [Key]
    [StringLength(10)]
    public string Id { get; set; } = null!;

    [StringLength(100)]
    public string Name { get; set; } = null!;
}

2. DbContextクラスの定義

PostgresContext.cs
using Microsoft.EntityFrameworkCore;

public partial class PostgresContext : DbContext
{
    public PostgresContext(DbContextOptions<PostgresContext> options)
        : base(options)
    {
    }

    public virtual DbSet<Product> Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        OnModelCreatingPartial(modelBuilder);
    }

    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}


CRUD操作

Productエンティティクラスを介して、Productテーブルを操作する例を以下に示します。

ProductRepository.cs
using Microsoft.EntityFrameworkCore;

public class ProductRepository
{
    private readonly PostgresContext _context;

    public ProductRepository(PostgresContext context)
    {
        _context = context;
    }

    // Create(作成)
    public async Task Crate(Product product)
    {
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
    }

    // Read(読み込み)選択条件:主キー
    public async Task<Product> FindById(string id)
    {
        return await _context.Products.SingleOrDefaultAsync(p => p.Id == id);
    }


    // Read(読み込み)選択条件:主キー以外
    public async Task<List<Product>> FindByName(string name)
    {
        return await _context.Products.Where(p => p.Name == name).ToListAsync();
    }

    // Update(更新)
    public async Task Update(Product product)
    {
        var target = await _context.Products.FirstOrDefaultAsync(p => p.Id == product.Id);
        target.Name = product.Name;
        await _context.SaveChangesAsync();
    }

    // Delete(削除)
    public async Task Delete(Product product)
    {
        _context.Products.Remove(product);
        await _context.SaveChangesAsync();
    }
}

拡張LINQメソッド

EF Core では、拡張LINQメソッドを自作するのをよく見かけます。
ここでは、よく実装されるであろう条件付き WHERE 句WhereIf)とページング処理の実装例を紹介します。
これらの拡張メソッドを利用することで、コード内で簡潔に扱うことができるかと思います。

拡張メソッド実装

LinqExtentions.cs
using System.Linq.Expressions;

public static class LinqExtentions
{
    /// <summary>
    /// 動的にWHERE文を追加
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="query"></param>
    /// <param name="condition">任意の条件式</param>
    /// <param name="predicate">条件がTrueの場合に適用する選択条件</param>
    /// <returns></returns>
    public static IQueryable<T> WhereIf<T>(this IQueryable<T> query, bool condition, Expression<Func<T, bool>> predicate)
    {
        return condition ? query.Where(predicate) : query;
    }

    /// <summary>
    /// ページング処理
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="query"></param>
    /// <param name="pageNumber">ページ番号</param>
    /// <param name="pageSize">ページサイズ</param>
    /// <returns></returns>
    /// <exception cref="ArgumentNullException"></exception>
    public static IQueryable<T> PageBy<T>(this IQueryable<T> query, int pageNumber, int pageSize)
    {
        return query.Skip((pageNumber - 1) * pageSize).Take(pageSize);
    }
}

使い方の例

ProductRepository.cs
using Microsoft.EntityFrameworkCore;

public class ProductRepository
{
    private readonly PostgresContext _context;

    public ProductRepository(PostgresContext context)
    {
        _context = context;
    }

    // WhereIfの利用例
    public async Task<List<Product>> FindByName(string name, bool flag)
    {
        // flagがtrueならnameに対して選択条件を付与する
        return await _context.Products.WhereIf(flag, p => p.Name == name).ToListAsync();
    }

    // ページングの利用例
    public async Task<List<Product>> GetAllByPagenation(int pageNumber, int pageSize)
    {
        // ページングを用いたデータ取得
        return await _context.Products.PageBy(pageNumber, pageSize).ToListAsync();
    }
}

まとめ

  • Ef Coreを用いることで、開発速度の向上やコードの可読性/保守性の向上が期待できます。
    ただし、LINQの扱いに慣れていない場合や複雑なSQLクエリを実装する場合は適さない場合があるかと思います。
  • マイグレーション機能を用いることで、コードファーストでのデータベース作成およびデータベースの変更履歴管理が可能となります。
  • スキャフォールド機能を用いることで、データベースファーストでのエンティティクラスやDBContextクラスを作成することが可能となります。

Discussion