はじめに

C#のO/Rマッパーを使ってみようと思い調べたところ下記のフレームワークが候補にあがりました。
まずは、Microsoft社製である「Entity Framework」を使ってみることにしました。

  • Entity Framework
  • Dapper

「Entity Framework」は2つの系統がありMicrosoftの比較サイトを参考に、よりモダンな「EF Core」を選択することにしました。

  • Entity Framework(EF)
  • Entity Framework Core(EF Core)

EF Coreとは、Microsoftのサイトより引用

Entity Framework Core は、.NET 用の最新のオブジェクト データベース マッパーです。 LINQ クエリ、変更の追跡、更新、スキーマの移行がサポートされています。 EF Core は、SQL Database (オンプレミスと Azure)、SQLite、MySQL、PostgreSQL、Azure Cosmos DB などの多くのデータベースに対応しています。

ツール

EF Coreには便利なコマンドツールが用意されており主にスキャフォールディングで使用されます。両方のツールで同じ機能が提供されてます。

  • EF Core パッケージ マネージャー コンソール ツール

    EF Core パッケージ マネージャー コンソール ツールは、Visual Studio のパッケージ マネージャー コンソールで実行されます。 Visual Studio で開発している場合、統合性に優れたこれらのツールの使用をお勧めします。

    インストール手順
    Visual Studioの「ツール」>「NuGet パッケージ マネージャー」>「パッケージ マネージャー コンソール」で、下記を実行する。

    Install-Package Microsoft.EntityFrameworkCore.Tools
    Update-Package Microsoft.EntityFrameworkCore.Tools
    
  • EF Core .NET CLI ツール

    EF Core .NET コマンド ライン インターフェイス (CLI) ツールは、クロス プラットフォームの .NET Core CLI ツールの拡張機能です。 これらのツールには、.NET Core SDK プロジェクトが必要です (プロジェクト ファイルに Sdk=”Microsoft.NET.Sdk” か同様のものが含まれる)。

    インストール手順
    コマンドプロンプトで、下記を実行する。

    dotnet tool install --global dotnet-ef
    cd プロジェクトフォルダ(*.csprojがある場所)
    dotnet add package Microsoft.EntityFrameworkCore.Design
    

開発手法

EF Coreは下記の開発手法をサポートしてます。

  • Code-First
    モデルクラスを作成し、それを基にデータベースのテーブルを作成する。
    イメージ
    手順
    コマンドプロンプトで、下記の.NET CLIを実行する
    dotnet ef migrations add InitialCreate
    dotnet ef database update
    
  • Database-First
    データベースにすでにテーブルがあり、それをリバースエンジニアリングしてモデルクラスを作成する。
    イメージ
    手順
    コマンドプロンプトで、下記の.NET CLIを実行する
    dotnet ef dbcontext scaffold <接続文字列> <プロバイダ(e.g. Microsoft.EntityFrameworkCore.SqlServer)> -o <生成したクラスファイルの出力先>
    
  • スキャフォールディング
    ASP.NET CoreのスキャフォールディングにEF Coreを組み合わせることもできます。
    イメージ
    手順
    Visual StudioのソリューションエクスプローラーでASP.NET Coreのプロジェクトを右クリックして「追加」>「新規スキャフォールディングアイテム」で、追加するアイテムを選択する。
    イメージ

EF Coreを使う

Microsoftのチュートリアルを基に、ASP.NET CoreでEF Coreを使ってみました。
ソースコードはGitHubリポジトリにあります。

環境

  • Windows 10 64bit
  • Visual Studio Community 2022
  • C#
  • .NET 7.0
  • ASP.NET Core Webアプリ(Model-View-Controller)
  • SQL Server Express LocalDB

構成

イメージ

プロジェクト作成

プロジェクトテンプレートは「ASP.NET Core Webアプリ(Model-View-Controller)」を選択します。
イメージ
プロジェクト名はWebApplicationEFCoreにします。
イメージ

Webサイトのスタイル設定

サイトのホームページ、レイアウト、メニューを設定します。

スタイル変更前(デフォルト)
イメージ

スタイル変更後
イメージ

サイトのホームページを設定します。
Views\Home\Index.cshtmlを下記の内容に置き換えます。

@{
    ViewData["Title"] = "Home Page";
}

<div class="jumbotron">
    <h1>Contoso University</h1>
</div>
<div class="row">
    <div class="col-md-4">
        <h2>Welcome to Contoso University</h2>
        <p>
            Contoso University is a sample application that
            demonstrates how to use Entity Framework Core in an
            ASP.NET Core MVC web application.
        </p>
    </div>
    <div class="col-md-4">
        <h2>Build it from scratch</h2>
        <p>You can build the application by following the steps in a series of tutorials.</p>
        <p><a class="btn btn-default" href="https://docs.asp.net/en/latest/data/ef-mvc/intro.html">See the tutorial &raquo;</a></p>
    </div>
    <div class="col-md-4">
        <h2>Download it</h2>
        <p>You can download the completed project from GitHub.</p>
        <p><a class="btn btn-default" href="https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/5cu-final">See project source code &raquo;</a></p>
    </div>
</div>

サイトのヘッダーを設定します。
Views\Shared\_Layout.cshtmlを下記のように修正します。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>@ViewData["Title"] - WebApplicationEFCore</title>
+    <title>@ViewData["Title"] - Contoso University</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/WebApplicationEFCore.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">WebApplicationEFCore</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="About">About</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link text-dark" asp-area="" asp-controller="Students" asp-action="Index">Students</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link text-dark" asp-area="" asp-controller="Courses" asp-action="Index">Courses</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link text-dark" asp-area="" asp-controller="Instructors" asp-action="Index">Instructors</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link text-dark" asp-area="" asp-controller="Departments" asp-action="Index">Departments</a>
+                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
-            &copy; 2023 - WebApplicationEFCore - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
+            &copy; 2023 - Contoso University - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

EF Coreのインストール

NuGetで下記のパッケージをインストールします。

パッケージ 説明
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore EF Core移行に関するエラー検出
Microsoft.EntityFrameworkCore.SqlServer SQL Serverのプロパイダ

Microsoft.AspNetCore.Diagnostics.EntityFrameworkCoreのパッケージには
Microsoft.EntityFrameworkCoreが含まれてます。
イメージ

モデルクラスの作成

Modelsフォルダにエンティティのクラスを作成します。
イメージ

Models
 ├─ Course.cs         // 新規に追加する
 ├─ Enrollment.cs     // 新規に追加する
 ├─ ErrorViewModel.cs // デフォルトからある
 └─ Student.cs        // 新規に追加する

Models\Student.cs

namespace WebApplicationEFCore.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
    }
}

Models\Enrollment.cs

namespace WebApplicationEFCore.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        public Grade? Grade { get; set; }
    }
}

Models\Course.cs

namespace WebApplicationEFCore.Models
{
    public class Course
    {
        public int CourseID { get; set; }
        public string Title { get; set; }
        public int Credits { get; set; }
    }
}

スキャフォールディングで自動生成

ASP.NET Coreのスキャフォールディングで、DBのStudentテーブルを、画面操作でCRUDできるようにファイルを自動生成します。

Visual StudioのソリューションエクスプローラーでControllersフォルダを右クリックして、
「追加」>「新規スキャフォールディングアイテム」を選択します。
イメージ
「Entity Frameworkを使用したビューがあるMVCコントローラー」を選択します。
イメージ
作成元となるモデルクラスを、ドロップダウンリストからStudentクラスを選択します。
イメージ
イメージ
コントローラー名は自動補完してくれます。
イメージ
データコンテキストのクラス名は自動補完してくれます。
イメージ
イメージ
「追加」をクリックすると、自動生成されます。
イメージ

スキャフォールディング後は下記のようになります。

/
├─ Controllers
|  └─ StudentsController.cs // 追加
├─ Data
|  └─ WebApplicationEFCoreContext.cs // 追加
├─ Views
|  └─ Students
|     ├─ Create.cshtml  // 追加
|     ├─ Delete.cshtml  // 追加
|     ├─ Details.cshtml // 追加
|     ├─ Edit.cshtml    // 追加
|     └─ Index.cshtml   // 追加
├─ appsettings.json // 変更
└─ Program.cs       // 変更
  • StudentテーブルのCRUD用のコントローラークラスControllers\StudentsController.cs、ビューファイルViews\Students\*.cshtmlが追加されます。
    これにより、サイトのヘッダーにある「Students」をクリックすると Controllers\StudentsController.csが呼び出され、CRUDの操作画面と紐付きます。
    イメージ

  • appsettings.jsonにデータベースへの接続情報が追記されます。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
+  "ConnectionStrings": {
+    "WebApplicationEFCoreContext": "Server=(localdb)\\mssqllocaldb;Database=WebApplicationEFCoreContext-1b96a727-f6dd-48ca-933e-fbfbe82af641;Trusted_Connection=True;MultipleActiveResultSets=true"
+  }
}
  • データコンテキストクラスData\WebApplicationEFCoreContext.csが追加されます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using WebApplicationEFCore.Models;

namespace WebApplicationEFCore.Data
{
    public class WebApplicationEFCoreContext : DbContext
    {
        public WebApplicationEFCoreContext (DbContextOptions<WebApplicationEFCoreContext> options)
            : base(options)
        {
        }

        public DbSet<WebApplicationEFCore.Models.Student> Student { get; set; } = default!;
    }
}
  • Program.csでデータコンテキストクラスをDIコンテナに追加します。
+ using Microsoft.EntityFrameworkCore;
+ using Microsoft.Extensions.DependencyInjection;
+ using WebApplicationEFCore.Data;
namespace WebApplicationEFCore
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
+            builder.Services.AddDbContext<WebApplicationEFCoreContext>(options =>
+                options.UseSqlServer(builder.Configuration.GetConnectionString("WebApplicationEFCoreContext") ?? throw new InvalidOperationException("Connection string 'WebApplicationEFCoreContext' not found.")));

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");

            app.Run();
        }
    }
}
  • 自動生成されたコントローラークラスControllers\StudentsController.csで、注入したデータコンテキストクラス経由でDB操作を行います。
namespace WebApplicationEFCore.Controllers
{
    public class StudentsController : Controller
    {
        private readonly WebApplicationEFCoreContext _context;

        public StudentsController(WebApplicationEFCoreContext context)
        {
            _context = context;
        }

        // GET: Students
        public async Task<IActionResult> Index()
        {
              return _context.Student != null ? 
                          View(await _context.Student.ToListAsync()) :
                          Problem("Entity set 'WebApplicationEFCoreContext.Student'  is null.");
        }
        ~~~ 省略 ~~~
    }
}

データベース設定

データベース例外フィルターの追加

EF Coreへの移行時にエラーが発生した場合、解決案をHTMLで返すように、Program.csAddDatabaseDeveloperPageExceptionFilter()メソッドを呼び出します。(未検証)

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using WebApplicationEFCore.Data;
namespace WebApplicationEFCore
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            builder.Services.AddDbContext<WebApplicationEFCoreContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("WebApplicationEFCoreContext") ?? throw new InvalidOperationException("Connection string 'WebApplicationEFCoreContext' not found.")));

+            builder.Services.AddDatabaseDeveloperPageExceptionFilter();

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");

            app.Run();
        }
    }
}

データコンテキストクラスの修正

データコンテキストクラスData\WebApplicationEFCoreContext.csCourseテーブルとEnrollmentテーブルを追加します。
EF CoreはデフォルトでDbSetのプロパティ名からテーブル名を作成しますが、 今回は、DbSetのプロパティ名は複数形、テーブル名は単数形と別名にします。
OnModelCreating()メソッドで下記のようにすると、DbSetのプロパティ名テーブル名を別名に定義できます。

using Microsoft.EntityFrameworkCore;
using WebApplicationEFCore.Models;

namespace WebApplicationEFCore.Data
{
    public class WebApplicationEFCoreContext : DbContext
    {
        public WebApplicationEFCoreContext (DbContextOptions<WebApplicationEFCoreContext> options)
            : base(options)
        {
        }

+        public DbSet<Course> Courses { get; set; } = default!;
+        public DbSet<Enrollment> Enrollments { get; set; } = default!;
-        public DbSet<Student> Student { get; set; } = default!;
+        public DbSet<Student> Students { get; set; } = default!;  // ★★★単数形「Student」を複数形「Students」にする★★★

+        protected override void OnModelCreating(ModelBuilder modelBuilder)
+        {
+            modelBuilder.Entity<Course>().ToTable("Course");
+            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
+            modelBuilder.Entity<Student>().ToTable("Student");
+        }
    }
}

DbSetのプロパティ名をStudentからStudentsに変更する場合は、
Studentプロパティを右クリックして「名前の変更」で変更すると、影響範囲のコントローラークラスも修正してくるので便利です。
イメージ

データを投入するクラスの追加

データベースにデータを投入するクラスData\DbInitializer.csを追加します。

using WebApplicationEFCore.Models;

namespace WebApplicationEFCore.Data
{
    public static class DbInitializer
    {
        public static void Initialize(WebApplicationEFCoreContext context)
        {
            context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student{FirstMidName="Carson",LastName="Alexander",    EnrollmentDate=DateTime.Parse("2005-09-01")},
                new Student{FirstMidName="Meredith",LastName="Alonso",    EnrollmentDate=DateTime.Parse("2002-09-01")},
                new Student{FirstMidName="Arturo",LastName="Anand",    EnrollmentDate=DateTime.Parse("2003-09-01")},
                new Student{FirstMidName="Gytis",LastName="Barzdukas",    EnrollmentDate=DateTime.Parse("2002-09-01")},
                new Student{FirstMidName="Yan",LastName="Li",    EnrollmentDate=DateTime.Parse("2002-09-01")},
                new Student{FirstMidName="Peggy",LastName="Justice",    EnrollmentDate=DateTime.Parse("2001-09-01")},
                new Student{FirstMidName="Laura",LastName="Norman",    EnrollmentDate=DateTime.Parse("2003-09-01")},
                new Student{FirstMidName="Nino",LastName="Olivetto",    EnrollmentDate=DateTime.Parse("2005-09-01")}
            };
            foreach (Student s in students)
            {
                context.Students.Add(s);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course{Title="Chemistry",Credits=3},
                new Course{Title="Microeconomics",Credits=3},
                new Course{Title="Macroeconomics",Credits=3},
                new Course{Title="Calculus",Credits=4},
                new Course{Title="Trigonometry",Credits=4},
                new Course{Title="Composition",Credits=3},
                new Course{Title="Literature",Credits=4}
            };
            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
                new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
                new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
                new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
                new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
                new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
                new Enrollment{StudentID=3,CourseID=1050},
                new Enrollment{StudentID=4,CourseID=1050},
                new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
                new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
                new Enrollment{StudentID=6,CourseID=1045},
                new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
            };
            foreach (Enrollment e in enrollments)
            {
                context.Enrollments.Add(e);
            }
            context.SaveChanges();
        }
    }
}

アプリ起動時にデータ投入するように、上記で追加したDbInitializer.Initialize()メソッドをProgram.csで呼び出します。

using Microsoft.EntityFrameworkCore;
using WebApplicationEFCore.Data;
namespace WebApplicationEFCore
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            builder.Services.AddDbContext<WebApplicationEFCoreContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("WebApplicationEFCoreContext") ?? throw new InvalidOperationException("Connection string 'WebApplicationEFCoreContext' not found.")));

            builder.Services.AddDatabaseDeveloperPageExceptionFilter();

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");

+            using (var scope = app.Services.CreateScope())
+            {
+                var services = scope.ServiceProvider;
+                try
+                {
+                    var context = services.GetRequiredService<WebApplicationEFCoreContext>();
+                    DbInitializer.Initialize(context);
+                }
+                catch (Exception ex)
+                {
+                    var logger = services.GetRequiredService<ILogger<Program>>();
+                    logger.LogError(ex, "An error occurred while seeding the database.");
+                }
+            }

            app.Run();
        }
    }
}

Webアプリの実行

Visual StudioでデバッグF5実行して、「SQL Serverオブジェクトエクスプローラー」を開くと、テーブルが作成されており、
イメージ

dbo.Studentを右クリックして「データの表示」を選択します。
イメージ
イメージ

Studentテーブルの内容が、Webアプリで確認できます。
イメージ
イメージ

一番上のレコードの「Edit」をクリックして更新すると、
イメージ
一覧も更新され、
イメージ
データベースと一致していることが分かります。
イメージ

ヘッダーの「About」「Courses」「Instructors」「Departments」は、今回は実装しません。クリックすると404エラーとなります。
イメージ

SQLクエリの確認

EF Coreが実行するSQLクエリは、デバッグコンソールに出力されます。

//「Student」一覧表示時の例
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (6ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [s].[ID], [s].[EnrollmentDate], [s].[FirstMidName], [s].[LastName]
      FROM [Student] AS [s]

//「Student」更新時の例
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[@p3='?' (DbType = Int32), @p0='?' (DbType = DateTime2), @p1='?' (Size = 4000), @p2='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      UPDATE [Student] SET [EnrollmentDate] = @p0, [FirstMidName] = @p1, [LastName] = @p2
      OUTPUT 1
      WHERE [ID] = @p3;

ソート、フィルター、ページング(ページネーション、ページャー)

  • 一覧のタイトルヘッダーのリンクで、ソートする
  • 検索ボックスで、フィルターする
  • 「前へ」「次へ」ボタンで、ページングする

ページング用にページャークラスPaginatedList.csを新規に作成します。
当該ページのデータをDBから取得してリスト化してくれます。

using Microsoft.EntityFrameworkCore;

namespace WebApplicationEFCore
{
    public class PaginatedList<T> : List<T>
    {
        public int PageIndex { get; private set; }
        public int TotalPages { get; private set; }

        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            PageIndex = pageIndex;
            TotalPages = (int)Math.Ceiling(count / (double)pageSize);

            this.AddRange(items);
        }

        public bool HasPreviousPage => PageIndex > 1;

        public bool HasNextPage => PageIndex < TotalPages;

        public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageIndex, int pageSize)
        {
            var count = await source.CountAsync();
            var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}

Views\Students\Index.cshtml

  • ヘッダーをリンク化して、コントローラークラスでソートできるようにします。
  • nameの検索ボックスを追加して、コントローラークラスでフィルターできるようにします。
  • PreviousNextボタンを追加して、コントローラークラスでページングできるようにします。
<!-- ★★★ページング対応★★★ -->
-@model IEnumerable<WebApplicationEFCore.Models.Student>
+@model PaginatedList<WebApplicationEFCore.Models.Student>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<!-- ★★★フィルター対応★★★ -->
+<form asp-action="Index" method="get">
+    <div class="form-actions no-color">
+        <p>
+            Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" />
+            <input type="submit" value="Search" class="btn btn-default" /> |
+            <a asp-action="Index">Back to Full List</a>
+        </p>
+    </div>
+</form>

<table class="table">
    <thead>
        <tr>
            <th>
         <!-- ★★★ソート対応、ページング対応★★★ -->
-                @Html.DisplayNameFor(model => model.LastName)
+                <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Last Name</a>
            </th>
            <th>
         <!-- ★★★ページング対応(ページャークラスにしたためハードコーディング)★★★ -->
-                @Html.DisplayNameFor(model => model.FirstMidName)
+                First Name
            </th>
            <th>
         <!-- ★★★ソート対応、ページング対応★★★ -->
-                @Html.DisplayNameFor(model => model.EnrollmentDate)
+                <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.HasNextPage ? "disabled" : "";
}

<!-- ★★★ページング対応★★★ -->
+<a asp-action="Index"
+   asp-route-sortOrder="@ViewData["CurrentSort"]"
+   asp-route-pageNumber="@(Model.PageIndex - 1)"
+   asp-route-currentFilter="@ViewData["CurrentFilter"]"
+   class="btn btn-default @prevDisabled">
+    Previous
+</a>
+<a asp-action="Index"
+   asp-route-sortOrder="@ViewData["CurrentSort"]"
+   asp-route-pageNumber="@(Model.PageIndex + 1)"
+   asp-route-currentFilter="@ViewData["CurrentFilter"]"
+   class="btn btn-default @nextDisabled">
+    Next
+</a>

コントローラークラスControllers\StudentsController.csで、ソート、フィルター、ページングできるように、Index()メソッドを下記に置き換えます。

// GET: Students
public async Task<IActionResult> Index(
    string sortOrder,
    string currentFilter,
    string searchString,
    int? pageNumber)
{
    ViewData["CurrentSort"] = sortOrder;
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";

    if (searchString != null)
    {
        pageNumber = 1;
    }
    else
    {
        searchString = currentFilter;
    }
    ViewData["CurrentFilter"] = searchString;

    var students = from s in _context.Students
                   select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        students = students.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    int pageSize = 3;
    return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));
}

Visual StudioでデバッグF5実行すると、ソート、フィルター、ページングができるようになってます。
イメージ

「Enrollment Date」でソートし、「i」でフィルターし、2ページ目を表示した例です。
イメージ

さいごに

EF Coreを使うと、生SQLクエリ(DDL、DML)を書かなくてもDBの操作ができ、Code-Firstは便利に感じました。ただ、EF CoreのバックグラウンドでSQLクエリを実行するので、何のSQLクエリが、いつ実行されているのかは、把握していたほうがデバッグやパフォーマンスのチューニング等に役に立つと思いました。 今回は簡単なデータ構成でしたが、もっと複雑になった場合、EF Coreの使い勝手はどうなるのか、色々なパターンで検討したいと思います。

参考サイト