ASP.NET Core WebアプリでDapperを使う

はじめに

前回は、Npgsqlを使ってPostgreSQLに接続し、Npgsql.EntityFrameworkCore.PostgreSQLという.NET用のORMを使ってデータのやり取りを行った。
今回は、この部分をDapperを使ってやり取りを行ってみる。

環境

Windows 11 Professional
Docker Desktop 4.34.3 (170107)
ASP.NET Core 8.0
PostgreSQL 17.0
Dapper 2.1.35

Dapperとは

https://github.com/DapperLib/Dapper

Dapperは、.NET環境で使用される軽量なORM(オブジェクト・リレーショナル・マッピング)ライブラリの一つである。

Microsoftが提供するEntity Frameworkとは異なり、Dapperは高速でシンプルな操作を特徴とし、開発者がSQLクエリを直接記述するスタイルをサポートする。

特徴

  • 高速で効率的: Dapperは、ADO.NETをラップする形で動作しており、SQLクエリの実行速度が速く、パフォーマンスの最適化が容易。

  • シンプルな構文: 複雑な設定を必要とせず、数行のコードでデータベース操作が可能。

  • データベースの柔軟な操作: SQLクエリを直接使用できるため、開発者はデータベースの制御を細かく行える。

  • マッピングが簡単: データベースの行データをC#オブジェクトに自動でマッピングしてくれるため、コードの可読性が高まる。

準備

PostgreSQLの準備

ASP.NET Core WebアプリでNpgsqlを使いPostgreSQLに接続する#準備 を参照する。

Dapperを入れる

  1. プロジェクトを右クリック、「NuGetパッケージの管理」を選択する
  2. Dapper」をインストールする
  3. インストール完了後、依存関係に 「Dapper」が入っていることを確認

Dapperを使ってPostgreSQLにアクセスをする

前回作成した、appsettings.jsonの接続情報を使う。
usersテーブルの一覧を列挙するということを行いたいので、リポジトリパターンで実装する。
※ASP.NET Coreのベストプラクティス的なものはわからないがとりあえず。

今回修正した内容は以下になる。
https://github.com/katsuobushiFPGA/ASPCoreNetSample/commit/1a1f17bf96879e17e03f76dd6394c0585a2a96f3

Modelsクラスを作成

前回は、Entityディレクトリを作成してその中に入れた。
今回は、Modelsに作るのが正解っぽさそう。
※この辺あまりよくわかっていないが、使用するライブラリによって違うかも。

プロパティと、テーブルのカラム名が一致するように定義した。
※一致しないように定義する場合は、EntityFrameworkでやっていたようにプロパティの前に、対応するカラム名をマッピング定義する必要がある。

FYI: https://qiita.com/hi_zacky/items/64fbc685e3beaac931e6

Models/User.cs
namespace WebApplication2.Models
{
    public class User
    {
        public int id { get; set; }  // ユーザーID
        public string username { get; set; }  // 名前
        public string email { get; set; }  // メールアドレス
        public string password_hash { get; set; } // パスワード
        public DateTime? created_at { get; set; } // 作成日時
    }
}

Repositoryパターンに必要なものの作成

Repositoryディレクトリを作成し以下のファイルを作成する。

Repository/IUserRepository.cs
using WebApplication2.Models;

namespace WebApplication2.Repository
{
    public interface IUserRepository
    {
        Task<IEnumerable<User>> GetUsersAsync();
        Task<User> GetUserByIdAsync(int id);
        System.Threading.Tasks.Task AddUserAsync(User user);
        System.Threading.Tasks.Task UpdateUserAsync(User user);
        System.Threading.Tasks.Task DeleteUserAsync(int id);
    }

}
Repository/UserRepository.cs
using Context;
using Dapper;
using WebApplication2.Models;

namespace WebApplication2.Repository
{
    public class UserRepository : IUserRepository
    {
        private readonly ApplicationDbContext _context;

        public UserRepository(ApplicationDbContext context)
        {
            _context = context;
        }

        public async Task<IEnumerable<User>> GetUsersAsync()
        {
            var sql = "SELECT * FROM users";
            return await _context.Connection.QueryAsync<User>(sql);
        }

        public async Task<User> GetUserByIdAsync(int id)
        {
            var sql = "SELECT * FROM users WHERE Id = @Id";
            return await _context.Connection.QueryFirstOrDefaultAsync<User>(sql, new { Id = id });
        }

        public async System.Threading.Tasks.Task AddUserAsync(User user)
        {
            var sql = "INSERT INTO users (username, email) VALUES (@username, @email)";
            await _context.Connection.ExecuteAsync(sql, user);
        }

        public async System.Threading.Tasks.Task UpdateUserAsync(User user)
        {
            var sql = "UPDATE users SET username = @username, email = @email WHERE Id = @Id";
            await _context.Connection.ExecuteAsync(sql, user);
        }

        public async System.Threading.Tasks.Task DeleteUserAsync(int id)
        {
            var sql = "DELETE FROM users WHERE Id = @Id";
            await _context.Connection.ExecuteAsync(sql, new { Id = id });
        }
    }

}

ApplicationDbContext.csの修正

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

namespace Context
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<Entity.User> Users { get; set; }
        public DbSet<Entity.Task> Tasks { get; set; }
        private IDbConnection _connection;

        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
        public IDbConnection Connection
        {
            get
            {
                if (_connection == null)
                {
                    _connection = Database.GetDbConnection();
                }
                return _connection;
            }
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Entity.User>()
                .ToTable("users");

            // tasksテーブルのUserへの外部キー制約を設定
            modelBuilder.Entity<Entity.Task>()
                .ToTable("tasks")
                .HasOne(t => t.User)
                .WithMany(u => u.Tasks)
                .HasForeignKey(t => t.UserId)
                .OnDelete(DeleteBehavior.Cascade);

            base.OnModelCreating(modelBuilder);
        }
    }
}

Program.csを修正

program.cs
using Microsoft.EntityFrameworkCore;
using Context;
using WebApplication2.Repository;

var builder = WebApplication.CreateBuilder(args);

// PostgreSQLへの接続を設定
var connectionString = builder.Configuration.GetConnectionString("PostgreSqlConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString));

// for Dapper DIコンテナへの登録
builder.Services.AddScoped<IUserRepository, UserRepository>();

// 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();

usersテーブルを取得するコントローラーを作成する

Controllers/UsersController.cs
using Microsoft.AspNetCore.Mvc;
using WebApplication2.Repository;

namespace WebApplication2.Controllers
{
    public class UsersController : Controller
    {
        private readonly IUserRepository _userRepository;

        public UsersController(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }

        // GET: users
        public async Task<IActionResult> Index()
        {
            var users = await _userRepository.GetUsersAsync();
            return View(users); // ビューにタスクのリストを渡す
        }

    }
}

コントローラーに対応するビューを作成

asp-actionのタグを設置しているが、対応するコントローラーのメソッドがないので動作しないことに注意。

Views/Users/Index.cshtml
@model IEnumerable<User>

@{
    ViewData["Title"] = "User List";
}

<h2>User List</h2>

<table class="table table-striped">
    <thead>
        <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Email</th>
            <th>Actions</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var user in Model)
        {
            <tr>
                <td>@user.id</td>
                <td>@user.username</td>
                <td>@user.email</td>
                <td>
                    <a asp-action="GetUserById" asp-route-id="@user.id" class="btn btn-info">View</a>
                    <a asp-action="UpdateUser" asp-route-id="@user.id" class="btn btn-warning">Edit</a>
                    <a asp-action="DeleteUser" asp-route-id="@user.id" class="btn btn-danger">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

動作確認

「デバッグ」→「デバッグの開始」より起動する。

https://localhost:7006/users にアクセスし、DBの内容が表示されるかを確認する。
以下のようになっていればOK

users-01

参考

おわりに

ASP.NET Core, Dapper, のベストプラクティス的なものがわからないので、これを読んで理解を進めたい。

また、DapperEntity Frameworkどちらが適しているかというのもの、あまり理解できていない。
これも下記の記事を読んでライブラリの特性を理解してみたいと思う。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。