Entity Framework Core

Entity Framework Core(EF Core)は、.NET向けの軽量・拡張可能・クロスプラットフォームORMです。Code First、Database First両方のアプローチをサポートし、SQL Server、Azure SQL、SQLite、PostgreSQL、MySQL等を支援し、LINQ統合による強力なクエリ機能を提供します。

ORMC#.NETLINQDatabaseMicrosoft

ライブラリ

Entity Framework Core

概要

Entity Framework Core(EF Core)は、.NET向けの軽量・拡張可能・クロスプラットフォームORMです。Code First、Database First両方のアプローチをサポートし、SQL Server、Azure SQL、SQLite、PostgreSQL、MySQL等を支援し、LINQ統合による強力なクエリ機能を提供します。

詳細

Entity Framework CoreはMicrosoftが開発した.NETエコシステムの標準ORMです。従来のEntity Frameworkを軽量化・モダン化し、クロスプラットフォーム対応を実現しました。LINQによる型安全なクエリ、強力なマイグレーション機能、Change Trackingによる効率的な更新処理を特徴とし、エンタープライズからクラウドネイティブアプリケーションまで幅広く利用されています。

主な特徴

  • LINQ統合: 型安全で直感的なクエリ構文
  • Code First/Database First: 柔軟な開発アプローチ
  • Change Tracking: 効率的なエンティティ状態管理
  • マイグレーション: 自動的なスキーマ変更管理
  • クロスプラットフォーム: Windows、Linux、macOSサポート

メリット・デメリット

メリット

  • .NET開発者にとって自然で統一された開発体験
  • LINQ による強力で型安全なクエリ機能
  • Visual Studioとの優れた統合とIntelliSense支援
  • Azureクラウドサービスとの深い統合
  • マイクロサービスアーキテクチャでの実績豊富

デメリット

  • .NET以外の環境では使用できない
  • 大量データ処理でのパフォーマンス課題
  • 複雑なオブジェクトグラフでのメモリ使用量増大
  • 学習コストが比較的高い(特にEntity関係の設定)

参考ページ

書き方の例

インストールと基本セットアップ

# .NET CLI での EF Core インストール
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.EntityFrameworkCore.Design

# PostgreSQL を使用する場合
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
// Program.cs (.NET 6+)
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// EF Core サービス登録
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();

// appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyAppDb;Trusted_Connection=true"
  }
}

基本的なCRUD操作(エンティティ定義、作成、読み取り、更新、削除)

// Models/User.cs
using System.ComponentModel.DataAnnotations;

public class User
{
    public int Id { get; set; }
    
    [Required]
    [MaxLength(100)]
    public string Name { get; set; } = string.Empty;
    
    [Required]
    [EmailAddress]
    public string Email { get; set; } = string.Empty;
    
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    
    // ナビゲーションプロパティ
    public ICollection<Post> Posts { get; set; } = new List<Post>();
}

// Models/Post.cs
public class Post
{
    public int Id { get; set; }
    
    [Required]
    [MaxLength(200)]
    public string Title { get; set; } = string.Empty;
    
    public string? Content { get; set; }
    
    public int AuthorId { get; set; }
    public User Author { get; set; } = null!;
    
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

// Data/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
    
    public DbSet<User> Users { get; set; }
    public DbSet<Post> Posts { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Fluent API 設定
        modelBuilder.Entity<User>(entity =>
        {
            entity.HasIndex(e => e.Email).IsUnique();
            entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
        });
        
        modelBuilder.Entity<Post>(entity =>
        {
            entity.HasOne(p => p.Author)
                  .WithMany(u => u.Posts)
                  .HasForeignKey(p => p.AuthorId)
                  .OnDelete(DeleteBehavior.Cascade);
        });
    }
}

// 基本的なCRUD操作
using Microsoft.EntityFrameworkCore;

public class UserService
{
    private readonly ApplicationDbContext _context;
    
    public UserService(ApplicationDbContext context)
    {
        _context = context;
    }
    
    // 作成
    public async Task<User> CreateUserAsync(string name, string email)
    {
        var user = new User { Name = name, Email = email };
        _context.Users.Add(user);
        await _context.SaveChangesAsync();
        return user;
    }
    
    // 読み取り
    public async Task<List<User>> GetAllUsersAsync()
    {
        return await _context.Users.ToListAsync();
    }
    
    public async Task<User?> GetUserByIdAsync(int id)
    {
        return await _context.Users.FindAsync(id);
    }
    
    // 更新
    public async Task<User?> UpdateUserAsync(int id, string name)
    {
        var user = await _context.Users.FindAsync(id);
        if (user != null)
        {
            user.Name = name;
            await _context.SaveChangesAsync();
        }
        return user;
    }
    
    // 削除
    public async Task<bool> DeleteUserAsync(int id)
    {
        var user = await _context.Users.FindAsync(id);
        if (user != null)
        {
            _context.Users.Remove(user);
            await _context.SaveChangesAsync();
            return true;
        }
        return false;
    }
}

高度なクエリとリレーションシップ

// 複雑なLINQクエリ
public class AdvancedQueryService
{
    private readonly ApplicationDbContext _context;
    
    public AdvancedQueryService(ApplicationDbContext context)
    {
        _context = context;
    }
    
    // 複合条件クエリ
    public async Task<List<User>> GetActiveUsersAsync()
    {
        return await _context.Users
            .Where(u => u.Posts.Any(p => p.CreatedAt >= DateTime.UtcNow.AddDays(-30)))
            .Where(u => u.Email.EndsWith("@company.com"))
            .OrderByDescending(u => u.CreatedAt)
            .Take(10)
            .ToListAsync();
    }
    
    // JOIN クエリ(Include)
    public async Task<List<User>> GetUsersWithPostsAsync()
    {
        return await _context.Users
            .Include(u => u.Posts)
            .ToListAsync();
    }
    
    // Select による射影
    public async Task<List<object>> GetUserSummaryAsync()
    {
        return await _context.Users
            .Select(u => new
            {
                u.Id,
                u.Name,
                u.Email,
                PostCount = u.Posts.Count(),
                LatestPost = u.Posts.OrderByDescending(p => p.CreatedAt).FirstOrDefault()!.Title
            })
            .ToListAsync();
    }
    
    // グループ化と集約
    public async Task<List<object>> GetPostCountsByUserAsync()
    {
        return await _context.Posts
            .GroupBy(p => p.Author.Name)
            .Select(g => new
            {
                AuthorName = g.Key,
                PostCount = g.Count(),
                LatestPostDate = g.Max(p => p.CreatedAt)
            })
            .OrderByDescending(x => x.PostCount)
            .ToListAsync();
    }
    
    // Raw SQL クエリ
    public async Task<List<User>> GetUsersByRawSqlAsync(string emailDomain)
    {
        return await _context.Users
            .FromSqlRaw("SELECT * FROM Users WHERE Email LIKE {0}", $"%@{emailDomain}")
            .ToListAsync();
    }
    
    // ストアドプロシージャ呼び出し
    public async Task<List<User>> GetUsersFromStoredProcAsync(int minPosts)
    {
        return await _context.Users
            .FromSqlRaw("EXEC GetActiveUsers @MinPosts = {0}", minPosts)
            .ToListAsync();
    }
}

マイグレーションとスキーマ管理

# 初回マイグレーション作成
dotnet ef migrations add InitialCreate

# マイグレーション適用
dotnet ef database update

# 新しいマイグレーション追加
dotnet ef migrations add AddPostsTable

# マイグレーション取り消し
dotnet ef migrations remove

# データベース削除
dotnet ef database drop
// カスタムマイグレーション
public partial class AddUserIndexes : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateIndex(
            name: "IX_Users_Email",
            table: "Users",
            column: "Email",
            unique: true);
            
        migrationBuilder.Sql("CREATE INDEX IX_Users_Name_Email ON Users (Name, Email)");
    }
    
    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropIndex(
            name: "IX_Users_Email",
            table: "Users");
            
        migrationBuilder.Sql("DROP INDEX IX_Users_Name_Email ON Users");
    }
}

// プログラムでのマイグレーション実行
public class DatabaseManager
{
    public static async Task MigrateDatabaseAsync(IServiceProvider serviceProvider)
    {
        using var scope = serviceProvider.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        await context.Database.MigrateAsync();
    }
}

パフォーマンス最適化と高度な機能

// トランザクション管理
public class TransactionService
{
    private readonly ApplicationDbContext _context;
    
    public TransactionService(ApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task<User> CreateUserWithPostsAsync(string name, string email, List<string> postTitles)
    {
        using var transaction = await _context.Database.BeginTransactionAsync();
        try
        {
            var user = new User { Name = name, Email = email };
            _context.Users.Add(user);
            await _context.SaveChangesAsync();
            
            foreach (var title in postTitles)
            {
                _context.Posts.Add(new Post 
                { 
                    Title = title, 
                    Content = $"Content for {title}",
                    AuthorId = user.Id 
                });
            }
            
            await _context.SaveChangesAsync();
            await transaction.CommitAsync();
            return user;
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

// バッチ操作
public class BatchOperationService
{
    private readonly ApplicationDbContext _context;
    
    public BatchOperationService(ApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task BulkInsertUsersAsync(List<(string Name, string Email)> userData)
    {
        var users = userData.Select(u => new User 
        { 
            Name = u.Name, 
            Email = u.Email 
        }).ToList();
        
        _context.Users.AddRange(users);
        await _context.SaveChangesAsync();
    }
    
    public async Task BulkUpdateUsersAsync(List<int> userIds, string newNamePrefix)
    {
        await _context.Users
            .Where(u => userIds.Contains(u.Id))
            .ExecuteUpdateAsync(u => u.SetProperty(x => x.Name, x => newNamePrefix + x.Name));
    }
}

// Change Tracking 最適化
public class OptimizedQueryService
{
    private readonly ApplicationDbContext _context;
    
    public OptimizedQueryService(ApplicationDbContext context)
    {
        _context = context;
    }
    
    // 読み取り専用クエリ(Change Tracking無効)
    public async Task<List<User>> GetUsersReadOnlyAsync()
    {
        return await _context.Users
            .AsNoTracking()
            .ToListAsync();
    }
    
    // 分割クエリ(Split Query)
    public async Task<List<User>> GetUsersWithPostsSplitAsync()
    {
        return await _context.Users
            .AsSplitQuery()
            .Include(u => u.Posts)
            .ToListAsync();
    }
    
    // コンパイル済みクエリ
    private static readonly Func<ApplicationDbContext, int, Task<User?>> GetUserByIdCompiled =
        EF.CompileAsyncQuery((ApplicationDbContext context, int id) =>
            context.Users.FirstOrDefault(u => u.Id == id));
    
    public async Task<User?> GetUserByIdOptimizedAsync(int id)
    {
        return await GetUserByIdCompiled(_context, id);
    }
}

フレームワーク統合と実用例

// ASP.NET Core Web API統合
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly UserService _userService;
    
    public UsersController(UserService userService)
    {
        _userService = userService;
    }
    
    [HttpGet]
    public async Task<ActionResult<List<User>>> GetUsers()
    {
        var users = await _userService.GetAllUsersAsync();
        return Ok(users);
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<User>> GetUser(int id)
    {
        var user = await _userService.GetUserByIdAsync(id);
        if (user == null)
            return NotFound();
        return Ok(user);
    }
    
    [HttpPost]
    public async Task<ActionResult<User>> CreateUser(CreateUserRequest request)
    {
        var user = await _userService.CreateUserAsync(request.Name, request.Email);
        return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
    }
}

// Minimal API 統合 (.NET 6+)
app.MapGet("/api/users", async (ApplicationDbContext db) =>
    await db.Users.ToListAsync());

app.MapGet("/api/users/{id}", async (int id, ApplicationDbContext db) =>
    await db.Users.FindAsync(id)
        is User user
            ? Results.Ok(user)
            : Results.NotFound());

app.MapPost("/api/users", async (CreateUserRequest request, ApplicationDbContext db) =>
{
    var user = new User { Name = request.Name, Email = request.Email };
    db.Users.Add(user);
    await db.SaveChangesAsync();
    return Results.Created($"/api/users/{user.Id}", user);
});

// Blazor Server統合
@page "/users"
@inject ApplicationDbContext DbContext

<h3>Users</h3>

@if (users == null)
{
    <p>Loading...</p>
}
else
{
    <table class="table">
        @foreach (var user in users)
        {
            <tr>
                <td>@user.Name</td>
                <td>@user.Email</td>
                <td>@user.Posts.Count posts</td>
            </tr>
        }
    </table>
}

@code {
    private List<User>? users;

    protected override async Task OnInitializedAsync()
    {
        users = await DbContext.Users
            .Include(u => u.Posts)
            .ToListAsync();
    }
}

// Background Service統合
public class DataProcessingService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    
    public DataProcessingService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = _serviceProvider.CreateScope();
            var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            
            // 定期的なデータ処理
            var unprocessedUsers = await context.Users
                .Where(u => !u.IsProcessed)
                .Take(100)
                .ToListAsync(stoppingToken);
            
            foreach (var user in unprocessedUsers)
            {
                user.IsProcessed = true;
                // 処理ロジック
            }
            
            await context.SaveChangesAsync(stoppingToken);
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}