Entity Framework Core

Entity Framework Core(EF Core)は「.NET向けの軽量・拡張可能・クロスプラットフォーム対応のオブジェクト関係マッピング(ORM)フレームワーク」として開発された、Microsoft公式のデータアクセステクノロジーです。.NET 5/6/7/8との完全統合により、LINQベースの直感的なクエリ記述、Code Firstアプローチによる自動スキーマ生成、包括的なマイグレーション機能を提供。SQL Server、MySQL、PostgreSQL、SQLite等の主要データベースをサポートし、現代的な.NET開発における包括的なデータ永続化ソリューションとして確固たる地位を築いています。

ORM.NETC#マイグレーションLINQCode First

GitHub概要

dotnet/efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.

スター14,213
ウォッチ887
フォーク3,282
作成日:2014年1月23日
言語:C#
ライセンス:MIT License

トピックス

aspnet-productc-sharpdatabasedotnet-coredotnet-frameworkdotnet-standardentity-frameworkhacktoberfestorm

スター履歴

dotnet/efcore Star History
データ取得日時: 2025/7/17 10:32

ライブラリ

Entity Framework Core

概要

Entity Framework Core(EF Core)は「.NET向けの軽量・拡張可能・クロスプラットフォーム対応のオブジェクト関係マッピング(ORM)フレームワーク」として開発された、Microsoft公式のデータアクセステクノロジーです。.NET 5/6/7/8との完全統合により、LINQベースの直感的なクエリ記述、Code Firstアプローチによる自動スキーマ生成、包括的なマイグレーション機能を提供。SQL Server、MySQL、PostgreSQL、SQLite等の主要データベースをサポートし、現代的な.NET開発における包括的なデータ永続化ソリューションとして確固たる地位を築いています。

詳細

Entity Framework Core 2025年版は、10年以上の進化により.NET エコシステムの中核データアクセス技術として成熟し続けています。LINQと完全統合された型安全なクエリシステムにより、SQLを直接記述することなく複雑なデータ操作を実現。Code Firstアプローチによる直感的なモデル定義から自動的なデータベーススキーマ生成、充実したマイグレーション機能により開発効率を大幅向上。ASP.NET Core、Blazor、.NET MAUIなどのモダンフレームワークとのシームレスな統合により、Web、デスクトップ、モバイルアプリケーション開発を包括的にサポートします。

主な特徴

  • LINQ統合: C#のLINQを使用した型安全で直感的なクエリ記述
  • Code Firstアプローチ: C#クラスからの自動データベーススキーマ生成
  • 包括的マイグレーション: 自動・手動両対応の本格的スキーマ変更管理
  • マルチDB対応: SQL Server、MySQL、PostgreSQL、SQLite等への統一API
  • .NET統合: ASP.NET Core、Blazor等との完全統合とクロスプラットフォーム対応
  • 高度な機能: 変更追跡、遅延読み込み、楽観的並行性制御サポート

メリット・デメリット

メリット

  • Microsoft公式サポートによる高い信頼性と長期保守体制
  • LINQとの完全統合による型安全で直感的なデータ操作
  • Code Firstアプローチによる高い開発効率とモデル中心設計
  • 包括的なマイグレーション機能による本格的なスキーマ管理
  • .NETエコシステム全体との優れた統合性とクロスプラットフォーム対応
  • 豊富なドキュメントと学習リソース、活発なコミュニティサポート

デメリット

  • .NET環境に特化しており他プラットフォームでの利用不可
  • 複雑なクエリでパフォーマンスが生SQLより劣る場合がある
  • 抽象化層により詳細なSQL制御が制限される場合
  • 大規模データ処理において他の専用ORMより重い場合
  • LINQへの依存によりSQL知識なしでの高度な最適化が困難
  • 自動生成コードへの依存によるブラックボックス化

参考ページ

書き方の例

基本セットアップ

# 新しい.NETプロジェクト作成
dotnet new web -n BlogApp
cd BlogApp

# Entity Framework Core パッケージ追加
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools

# SQLiteを使用する場合
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
// Program.cs - .NET 6以降のスタイル
using Microsoft.EntityFrameworkCore;
using BlogApp.Data;

var builder = WebApplication.CreateBuilder(args);

// Entity Framework Coreの設定
builder.Services.AddDbContext<BlogContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// 開発時のサービス追加
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

var app = builder.Build();

// 開発環境でのマイグレーション自動適用
if (app.Environment.IsDevelopment())
{
    using var scope = app.Services.CreateScope();
    var context = scope.ServiceProvider.GetRequiredService<BlogContext>();
    await context.Database.MigrateAsync();
}

app.Run();
// appsettings.json - 接続文字列設定
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=BlogAppDb;Trusted_Connection=true;MultipleActiveResultSets=true",
    "SQLiteConnection": "Data Source=blog.db"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore": "Information"
    }
  }
}

モデル定義と基本操作

// Models/Blog.cs - ブログエンティティ
using System.ComponentModel.DataAnnotations;

namespace BlogApp.Models;

public class Blog
{
    public int BlogId { get; set; }
    
    [Required]
    [StringLength(200)]
    public string Title { get; set; } = string.Empty;
    
    [StringLength(1000)]
    public string? Description { get; set; }
    
    public string Url { get; set; } = string.Empty;
    
    public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
    
    public DateTime? UpdatedDate { get; set; }
    
    // ナビゲーションプロパティ
    public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
    
    public virtual ICollection<Tag> Tags { get; set; } = new List<Tag>();
}

// Models/Post.cs - 投稿エンティティ
public class Post
{
    public int PostId { get; set; }
    
    [Required]
    [StringLength(300)]
    public string Title { get; set; } = string.Empty;
    
    public string Content { get; set; } = string.Empty;
    
    public DateTime PublishedDate { get; set; } = DateTime.UtcNow;
    
    public DateTime? UpdatedDate { get; set; }
    
    public bool IsPublished { get; set; } = false;
    
    // 外部キー
    public int BlogId { get; set; }
    
    // ナビゲーションプロパティ
    public virtual Blog Blog { get; set; } = null!;
    
    public virtual ICollection<Comment> Comments { get; set; } = new List<Comment>();
    
    public virtual ICollection<Tag> Tags { get; set; } = new List<Tag>();
}

// Models/Comment.cs - コメントエンティティ
public class Comment
{
    public int CommentId { get; set; }
    
    [Required]
    public string Author { get; set; } = string.Empty;
    
    [Required]
    [EmailAddress]
    public string Email { get; set; } = string.Empty;
    
    public string Content { get; set; } = string.Empty;
    
    public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
    
    public bool IsApproved { get; set; } = false;
    
    // 外部キー
    public int PostId { get; set; }
    
    // ナビゲーションプロパティ
    public virtual Post Post { get; set; } = null!;
}

// Models/Tag.cs - タグエンティティ
public class Tag
{
    public int TagId { get; set; }
    
    [Required]
    [StringLength(50)]
    public string Name { get; set; } = string.Empty;
    
    [StringLength(200)]
    public string? Description { get; set; }
    
    // 多対多のナビゲーションプロパティ
    public virtual ICollection<Blog> Blogs { get; set; } = new List<Blog>();
    
    public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
}

// Data/BlogContext.cs - データベースコンテキスト
using Microsoft.EntityFrameworkCore;
using BlogApp.Models;

namespace BlogApp.Data;

public class BlogContext : DbContext
{
    public BlogContext(DbContextOptions<BlogContext> options) : base(options)
    {
    }
    
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
    public DbSet<Comment> Comments { get; set; }
    public DbSet<Tag> Tags { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        // ブログ設定
        modelBuilder.Entity<Blog>(entity =>
        {
            entity.HasKey(e => e.BlogId);
            entity.Property(e => e.Title).IsRequired().HasMaxLength(200);
            entity.Property(e => e.Url).IsRequired();
            entity.HasIndex(e => e.Url).IsUnique();
        });
        
        // 投稿設定
        modelBuilder.Entity<Post>(entity =>
        {
            entity.HasKey(e => e.PostId);
            entity.Property(e => e.Title).IsRequired().HasMaxLength(300);
            entity.Property(e => e.Content).HasColumnType("text");
            
            // ブログとの一対多リレーション
            entity.HasOne(p => p.Blog)
                  .WithMany(b => b.Posts)
                  .HasForeignKey(p => p.BlogId)
                  .OnDelete(DeleteBehavior.Cascade);
        });
        
        // コメント設定
        modelBuilder.Entity<Comment>(entity =>
        {
            entity.HasKey(e => e.CommentId);
            entity.Property(e => e.Author).IsRequired();
            entity.Property(e => e.Email).IsRequired();
            
            // 投稿との一対多リレーション
            entity.HasOne(c => c.Post)
                  .WithMany(p => p.Comments)
                  .HasForeignKey(c => c.PostId)
                  .OnDelete(DeleteBehavior.Cascade);
        });
        
        // タグ設定
        modelBuilder.Entity<Tag>(entity =>
        {
            entity.HasKey(e => e.TagId);
            entity.Property(e => e.Name).IsRequired().HasMaxLength(50);
            entity.HasIndex(e => e.Name).IsUnique();
        });
        
        // 多対多リレーション設定(Blog-Tag)
        modelBuilder.Entity<Blog>()
            .HasMany(b => b.Tags)
            .WithMany(t => t.Blogs)
            .UsingEntity<Dictionary<string, object>>(
                "BlogTag",
                j => j.HasOne<Tag>().WithMany().HasForeignKey("TagId"),
                j => j.HasOne<Blog>().WithMany().HasForeignKey("BlogId"));
        
        // 多対多リレーション設定(Post-Tag)
        modelBuilder.Entity<Post>()
            .HasMany(p => p.Tags)
            .WithMany(t => t.Posts)
            .UsingEntity<Dictionary<string, object>>(
                "PostTag",
                j => j.HasOne<Tag>().WithMany().HasForeignKey("TagId"),
                j => j.HasOne<Post>().WithMany().HasForeignKey("PostId"));
    }
}

// Services/BlogService.cs - ビジネスロジック
public class BlogService
{
    private readonly BlogContext _context;
    
    public BlogService(BlogContext context)
    {
        _context = context;
    }
    
    // ブログ作成
    public async Task<Blog> CreateBlogAsync(string title, string description, string url)
    {
        var blog = new Blog
        {
            Title = title,
            Description = description,
            Url = url,
            CreatedDate = DateTime.UtcNow
        };
        
        _context.Blogs.Add(blog);
        await _context.SaveChangesAsync();
        
        return blog;
    }
    
    // ブログ取得(投稿含む)
    public async Task<Blog?> GetBlogWithPostsAsync(int blogId)
    {
        return await _context.Blogs
            .Include(b => b.Posts)
                .ThenInclude(p => p.Comments)
            .Include(b => b.Tags)
            .FirstOrDefaultAsync(b => b.BlogId == blogId);
    }
    
    // 全ブログ取得
    public async Task<List<Blog>> GetAllBlogsAsync()
    {
        return await _context.Blogs
            .Include(b => b.Posts)
            .OrderBy(b => b.Title)
            .ToListAsync();
    }
    
    // ブログ更新
    public async Task<Blog?> UpdateBlogAsync(int blogId, string title, string description)
    {
        var blog = await _context.Blogs.FindAsync(blogId);
        if (blog == null) return null;
        
        blog.Title = title;
        blog.Description = description;
        blog.UpdatedDate = DateTime.UtcNow;
        
        await _context.SaveChangesAsync();
        return blog;
    }
    
    // ブログ削除
    public async Task<bool> DeleteBlogAsync(int blogId)
    {
        var blog = await _context.Blogs.FindAsync(blogId);
        if (blog == null) return false;
        
        _context.Blogs.Remove(blog);
        await _context.SaveChangesAsync();
        return true;
    }
}

高度なクエリ操作

// Services/PostQueryService.cs - 高度なクエリ例
public class PostQueryService
{
    private readonly BlogContext _context;
    
    public PostQueryService(BlogContext context)
    {
        _context = context;
    }
    
    // 複雑な条件検索
    public async Task<List<Post>> SearchPostsAsync(string? keyword, int? blogId, bool? isPublished, DateTime? fromDate, DateTime? toDate)
    {
        var query = _context.Posts
            .Include(p => p.Blog)
            .Include(p => p.Tags)
            .AsQueryable();
        
        // 動的条件追加
        if (!string.IsNullOrWhiteSpace(keyword))
        {
            query = query.Where(p => p.Title.Contains(keyword) || p.Content.Contains(keyword));
        }
        
        if (blogId.HasValue)
        {
            query = query.Where(p => p.BlogId == blogId.Value);
        }
        
        if (isPublished.HasValue)
        {
            query = query.Where(p => p.IsPublished == isPublished.Value);
        }
        
        if (fromDate.HasValue)
        {
            query = query.Where(p => p.PublishedDate >= fromDate.Value);
        }
        
        if (toDate.HasValue)
        {
            query = query.Where(p => p.PublishedDate <= toDate.Value);
        }
        
        return await query
            .OrderByDescending(p => p.PublishedDate)
            .ToListAsync();
    }
    
    // 集計クエリ
    public async Task<PostStatistics> GetPostStatisticsAsync()
    {
        var stats = await _context.Posts
            .GroupBy(p => 1) // 全体をグループ化
            .Select(g => new PostStatistics
            {
                TotalPosts = g.Count(),
                PublishedPosts = g.Count(p => p.IsPublished),
                DraftPosts = g.Count(p => !p.IsPublished),
                TotalComments = g.Sum(p => p.Comments.Count),
                AverageCommentsPerPost = g.Average(p => p.Comments.Count),
                LastPostDate = g.Max(p => p.PublishedDate)
            })
            .FirstOrDefaultAsync();
        
        return stats ?? new PostStatistics();
    }
    
    // 人気投稿取得(コメント数順)
    public async Task<List<Post>> GetPopularPostsAsync(int count = 10)
    {
        return await _context.Posts
            .Include(p => p.Blog)
            .Include(p => p.Comments)
            .Where(p => p.IsPublished)
            .OrderByDescending(p => p.Comments.Count)
            .ThenByDescending(p => p.PublishedDate)
            .Take(count)
            .ToListAsync();
    }
    
    // タグ別投稿取得
    public async Task<List<Post>> GetPostsByTagAsync(string tagName)
    {
        return await _context.Posts
            .Include(p => p.Blog)
            .Include(p => p.Tags)
            .Where(p => p.Tags.Any(t => t.Name == tagName) && p.IsPublished)
            .OrderByDescending(p => p.PublishedDate)
            .ToListAsync();
    }
    
    // ページング対応の投稿取得
    public async Task<PagedResult<Post>> GetPagedPostsAsync(int pageNumber, int pageSize, int? blogId = null)
    {
        var query = _context.Posts
            .Include(p => p.Blog)
            .Include(p => p.Tags)
            .AsQueryable();
        
        if (blogId.HasValue)
        {
            query = query.Where(p => p.BlogId == blogId.Value);
        }
        
        var totalCount = await query.CountAsync();
        
        var posts = await query
            .Where(p => p.IsPublished)
            .OrderByDescending(p => p.PublishedDate)
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();
        
        return new PagedResult<Post>
        {
            Items = posts,
            TotalCount = totalCount,
            PageNumber = pageNumber,
            PageSize = pageSize,
            TotalPages = (int)Math.Ceiling((double)totalCount / pageSize)
        };
    }
    
    // 生SQLクエリ使用例
    public async Task<List<BlogStatistics>> GetBlogStatisticsRawSqlAsync()
    {
        return await _context.Database
            .SqlQueryRaw<BlogStatistics>(@"
                SELECT 
                    b.BlogId,
                    b.Title,
                    COUNT(p.PostId) as PostCount,
                    COUNT(CASE WHEN p.IsPublished = 1 THEN 1 END) as PublishedPostCount,
                    COALESCE(SUM(c.CommentCount), 0) as TotalComments
                FROM Blogs b
                LEFT JOIN Posts p ON b.BlogId = p.BlogId
                LEFT JOIN (
                    SELECT PostId, COUNT(*) as CommentCount 
                    FROM Comments 
                    GROUP BY PostId
                ) c ON p.PostId = c.PostId
                GROUP BY b.BlogId, b.Title
                ORDER BY PostCount DESC")
            .ToListAsync();
    }
}

// DTOs/PagedResult.cs - ページング結果
public class PagedResult<T>
{
    public List<T> Items { get; set; } = new();
    public int TotalCount { get; set; }
    public int PageNumber { get; set; }
    public int PageSize { get; set; }
    public int TotalPages { get; set; }
    public bool HasPreviousPage => PageNumber > 1;
    public bool HasNextPage => PageNumber < TotalPages;
}

// DTOs/PostStatistics.cs - 投稿統計
public class PostStatistics
{
    public int TotalPosts { get; set; }
    public int PublishedPosts { get; set; }
    public int DraftPosts { get; set; }
    public int TotalComments { get; set; }
    public double AverageCommentsPerPost { get; set; }
    public DateTime LastPostDate { get; set; }
}

// DTOs/BlogStatistics.cs - ブログ統計
public class BlogStatistics
{
    public int BlogId { get; set; }
    public string Title { get; set; } = string.Empty;
    public int PostCount { get; set; }
    public int PublishedPostCount { get; set; }
    public int TotalComments { get; set; }
}

リレーション操作

// Services/RelationshipService.cs - リレーション操作の例
public class RelationshipService
{
    private readonly BlogContext _context;
    
    public RelationshipService(BlogContext context)
    {
        _context = context;
    }
    
    // 投稿作成とタグ関連付け
    public async Task<Post> CreatePostWithTagsAsync(int blogId, string title, string content, List<string> tagNames)
    {
        using var transaction = await _context.Database.BeginTransactionAsync();
        
        try
        {
            // ブログ存在確認
            var blog = await _context.Blogs.FindAsync(blogId);
            if (blog == null)
                throw new ArgumentException("Blog not found", nameof(blogId));
            
            // 投稿作成
            var post = new Post
            {
                Title = title,
                Content = content,
                BlogId = blogId,
                PublishedDate = DateTime.UtcNow,
                IsPublished = true
            };
            
            _context.Posts.Add(post);
            await _context.SaveChangesAsync(); // IDを取得するために保存
            
            // タグ処理
            foreach (var tagName in tagNames.Distinct())
            {
                var tag = await _context.Tags
                    .FirstOrDefaultAsync(t => t.Name == tagName);
                
                if (tag == null)
                {
                    // 新しいタグ作成
                    tag = new Tag { Name = tagName };
                    _context.Tags.Add(tag);
                    await _context.SaveChangesAsync();
                }
                
                // 投稿とタグを関連付け
                post.Tags.Add(tag);
            }
            
            await _context.SaveChangesAsync();
            await transaction.CommitAsync();
            
            return post;
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
    
    // コメント追加
    public async Task<Comment> AddCommentToPostAsync(int postId, string author, string email, string content)
    {
        var post = await _context.Posts.FindAsync(postId);
        if (post == null)
            throw new ArgumentException("Post not found", nameof(postId));
        
        var comment = new Comment
        {
            PostId = postId,
            Author = author,
            Email = email,
            Content = content,
            CreatedDate = DateTime.UtcNow,
            IsApproved = false // 管理者承認待ち
        };
        
        _context.Comments.Add(comment);
        await _context.SaveChangesAsync();
        
        return comment;
    }
    
    // ブログ間での投稿移動
    public async Task<bool> MovePostToBlogAsync(int postId, int newBlogId)
    {
        var post = await _context.Posts.FindAsync(postId);
        var newBlog = await _context.Blogs.FindAsync(newBlogId);
        
        if (post == null || newBlog == null)
            return false;
        
        post.BlogId = newBlogId;
        post.UpdatedDate = DateTime.UtcNow;
        
        await _context.SaveChangesAsync();
        return true;
    }
    
    // 投稿からタグ削除
    public async Task<bool> RemoveTagFromPostAsync(int postId, int tagId)
    {
        var post = await _context.Posts
            .Include(p => p.Tags)
            .FirstOrDefaultAsync(p => p.PostId == postId);
        
        if (post == null) return false;
        
        var tag = post.Tags.FirstOrDefault(t => t.TagId == tagId);
        if (tag == null) return false;
        
        post.Tags.Remove(tag);
        await _context.SaveChangesAsync();
        
        return true;
    }
    
    // ブログに複数投稿一括追加
    public async Task<List<Post>> AddMultiplePostsToBlogAsync(int blogId, List<(string Title, string Content)> posts)
    {
        var blog = await _context.Blogs.FindAsync(blogId);
        if (blog == null)
            throw new ArgumentException("Blog not found", nameof(blogId));
        
        var newPosts = posts.Select(p => new Post
        {
            Title = p.Title,
            Content = p.Content,
            BlogId = blogId,
            PublishedDate = DateTime.UtcNow,
            IsPublished = false // ドラフトとして作成
        }).ToList();
        
        _context.Posts.AddRange(newPosts);
        await _context.SaveChangesAsync();
        
        return newPosts;
    }
    
    // 関連データの一括削除
    public async Task<bool> DeleteBlogWithAllRelatedDataAsync(int blogId)
    {
        using var transaction = await _context.Database.BeginTransactionAsync();
        
        try
        {
            var blog = await _context.Blogs
                .Include(b => b.Posts)
                    .ThenInclude(p => p.Comments)
                .Include(b => b.Posts)
                    .ThenInclude(p => p.Tags)
                .FirstOrDefaultAsync(b => b.BlogId == blogId);
            
            if (blog == null) return false;
            
            // カスケード削除が設定されているため、ブログを削除すれば
            // 関連する投稿とコメントも自動削除される
            _context.Blogs.Remove(blog);
            await _context.SaveChangesAsync();
            
            await transaction.CommitAsync();
            return true;
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

実用例

// Controllers/BlogController.cs - ASP.NET Core コントローラー例
using Microsoft.AspNetCore.Mvc;
using BlogApp.Services;
using BlogApp.Models;

[ApiController]
[Route("api/[controller]")]
public class BlogController : ControllerBase
{
    private readonly BlogService _blogService;
    private readonly PostQueryService _postQueryService;
    private readonly RelationshipService _relationshipService;
    
    public BlogController(
        BlogService blogService, 
        PostQueryService postQueryService,
        RelationshipService relationshipService)
    {
        _blogService = blogService;
        _postQueryService = postQueryService;
        _relationshipService = relationshipService;
    }
    
    // 全ブログ取得
    [HttpGet]
    public async Task<ActionResult<List<Blog>>> GetBlogs()
    {
        var blogs = await _blogService.GetAllBlogsAsync();
        return Ok(blogs);
    }
    
    // ブログ詳細取得
    [HttpGet("{id}")]
    public async Task<ActionResult<Blog>> GetBlog(int id)
    {
        var blog = await _blogService.GetBlogWithPostsAsync(id);
        if (blog == null)
            return NotFound();
        
        return Ok(blog);
    }
    
    // ブログ作成
    [HttpPost]
    public async Task<ActionResult<Blog>> CreateBlog([FromBody] CreateBlogRequest request)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);
        
        try
        {
            var blog = await _blogService.CreateBlogAsync(
                request.Title, 
                request.Description, 
                request.Url);
            
            return CreatedAtAction(nameof(GetBlog), new { id = blog.BlogId }, blog);
        }
        catch (Exception ex)
        {
            return BadRequest(new { message = ex.Message });
        }
    }
    
    // ブログ更新
    [HttpPut("{id}")]
    public async Task<ActionResult<Blog>> UpdateBlog(int id, [FromBody] UpdateBlogRequest request)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);
        
        var blog = await _blogService.UpdateBlogAsync(id, request.Title, request.Description);
        if (blog == null)
            return NotFound();
        
        return Ok(blog);
    }
    
    // ブログ削除
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteBlog(int id)
    {
        var result = await _relationshipService.DeleteBlogWithAllRelatedDataAsync(id);
        if (!result)
            return NotFound();
        
        return NoContent();
    }
    
    // ブログの投稿検索
    [HttpGet("{id}/posts")]
    public async Task<ActionResult<PagedResult<Post>>> GetBlogPosts(
        int id,
        [FromQuery] int page = 1,
        [FromQuery] int size = 10,
        [FromQuery] string? keyword = null)
    {
        var result = await _postQueryService.GetPagedPostsAsync(page, size, id);
        return Ok(result);
    }
    
    // 投稿作成
    [HttpPost("{id}/posts")]
    public async Task<ActionResult<Post>> CreatePost(int id, [FromBody] CreatePostRequest request)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);
        
        try
        {
            var post = await _relationshipService.CreatePostWithTagsAsync(
                id, 
                request.Title, 
                request.Content, 
                request.Tags ?? new List<string>());
            
            return CreatedAtAction("GetPost", "Post", new { id = post.PostId }, post);
        }
        catch (ArgumentException ex)
        {
            return BadRequest(new { message = ex.Message });
        }
    }
}

// DTOs/Requests.cs - リクエストDTO
public class CreateBlogRequest
{
    [Required]
    [StringLength(200)]
    public string Title { get; set; } = string.Empty;
    
    [StringLength(1000)]
    public string? Description { get; set; }
    
    [Required]
    [Url]
    public string Url { get; set; } = string.Empty;
}

public class UpdateBlogRequest
{
    [Required]
    [StringLength(200)]
    public string Title { get; set; } = string.Empty;
    
    [StringLength(1000)]
    public string? Description { get; set; }
}

public class CreatePostRequest
{
    [Required]
    [StringLength(300)]
    public string Title { get; set; } = string.Empty;
    
    [Required]
    public string Content { get; set; } = string.Empty;
    
    public List<string>? Tags { get; set; }
}

// マイグレーション管理コマンド例
/*
# 初期マイグレーション作成
dotnet ef migrations add InitialCreate

# データベース更新
dotnet ef database update

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

# マイグレーション一覧表示
dotnet ef migrations list

# マイグレーション削除(最新のみ)
dotnet ef migrations remove

# 特定マイグレーションまで戻す
dotnet ef database update AddTagsAndComments

# スクリプト生成(本番用)
dotnet ef migrations script --output migration.sql

# バンドル作成(実行可能ファイル)
dotnet ef migrations bundle --output efbundle.exe
*/

// Startup.cs または Program.cs での高度な設定例
public void ConfigureServices(IServiceCollection services)
{
    // Entity Framework Core 設定
    services.AddDbContext<BlogContext>(options =>
    {
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), 
            sqlOptions =>
            {
                // 接続タイムアウト設定
                sqlOptions.CommandTimeout(30);
                
                // マイグレーションアセンブリ指定
                sqlOptions.MigrationsAssembly("BlogApp.Migrations");
                
                // リトライ設定
                sqlOptions.EnableRetryOnFailure(
                    maxRetryCount: 3,
                    maxRetryDelay: TimeSpan.FromSeconds(5),
                    errorNumbersToAdd: null);
            });
        
        // 開発環境での詳細ログ
        if (Environment.IsDevelopment())
        {
            options.EnableSensitiveDataLogging();
            options.EnableDetailedErrors();
        }
        
        // 変更追跡の最適化
        options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
    });
    
    // サービス登録
    services.AddScoped<BlogService>();
    services.AddScoped<PostQueryService>();
    services.AddScoped<RelationshipService>();
}