Dapper
Dapperは.NET向けの軽量で高性能なマイクロORM(Object-Relational Mapping)ライブラリで、「.NETのためのシンプルなオブジェクトマッパー」として開発されました。Stack Overflowが開発・運用する大規模トラフィックサイトで実際に使用され、実証されたパフォーマンスを持つライブラリです。生のSQLを記述しながらもC#オブジェクトとの自動マッピングを提供し、Entity FrameworkやNHibernateと比較して圧倒的に高速な実行速度と低いメモリ使用量を実現する、実用性重視のデータアクセスソリューションです。
GitHub概要
DapperLib/Dapper
Dapper - a simple object mapper for .Net
トピックス
スター履歴
ライブラリ
Dapper
概要
Dapperは.NET向けの軽量で高性能なマイクロORM(Object-Relational Mapping)ライブラリで、「.NETのためのシンプルなオブジェクトマッパー」として開発されました。Stack Overflowが開発・運用する大規模トラフィックサイトで実際に使用され、実証されたパフォーマンスを持つライブラリです。生のSQLを記述しながらもC#オブジェクトとの自動マッピングを提供し、Entity FrameworkやNHibernateと比較して圧倒的に高速な実行速度と低いメモリ使用量を実現する、実用性重視のデータアクセスソリューションです。
詳細
Dapper 2025年版は.NET 8+の最新機能を活用し、Source Generatorによるコンパイル時最適化やAsync/Awaitパターンの完全サポートにより、さらなる性能向上を実現しています。IDbConnectionの拡張メソッドとして実装されているため、既存のADO.NETコードとの統合が容易で、学習コストを最小限に抑えながら導入できます。プリペアドステートメント、パラメータ化クエリ、バルク操作、マルチマッピング機能を標準でサポートし、SQLインジェクション対策と型安全性を両立します。複雑なORMの設定やマッピング定義は不要で、直感的なAPIによる高速開発を可能にします。
主な特徴
- 超高速実行: ADO.NETに近い性能で最小限のオーバーヘッド
- シンプルAPI: 学習コストが低く直感的なメソッド群
- 型安全: C#の型システムを活用したコンパイル時チェック
- マルチデータベース: SQL Server、MySQL、PostgreSQL、SQLite対応
- 非同期サポート: 完全なAsync/Await パターン対応
- 豊富な機能: バルク操作、マルチマッピング、ストアドプロシージャ
メリット・デメリット
メリット
- Entity FrameworkやNHibernateより大幅に高速な実行性能
- 軽量で最小限の依存関係、プロジェクトサイズへの影響が少ない
- 生のSQLを直接記述でき、複雑なクエリも思い通りに実装可能
- 既存のADO.NETコードとの統合が簡単で段階的移行が可能
- 豊富なドキュメントとコミュニティサポート、Stack Overflow実証済み
- .NET Coreから.NET 8+まで幅広いバージョンサポート
デメリット
- 完全なORMではないため、リレーションシップ管理は手動実装が必要
- 自動マイグレーション、Change Tracking機能が提供されない
- 複雑なオブジェクトグラフの操作には不向き
- SQL知識が必要で、LINQ to SQLのような高レベル抽象化は未提供
- コードファースト開発よりもデータベースファースト開発に適している
- 大規模チーム開発でのSQL品質管理に追加考慮が必要
参考ページ
書き方の例
基本セットアップ
using Dapper;
using System.Data;
using System.Data.SqlClient;
// 接続文字列の設定
private readonly string connectionString = "Server=localhost;Database=TestDB;Trusted_Connection=true;";
// データベース接続の取得
private IDbConnection GetConnection()
{
return new SqlConnection(connectionString);
}
// 基本的な使用例
public void BasicExample()
{
using var connection = GetConnection();
connection.Open();
// シンプルなクエリ実行
var users = connection.Query<User>("SELECT * FROM Users WHERE Age > @age", new { age = 18 });
foreach (var user in users)
{
Console.WriteLine($"Name: {user.Name}, Email: {user.Email}");
}
}
モデル定義と基本操作
// ユーザーモデルの定義
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public int Age { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
// ユーザーサービスクラス
public class UserService
{
private readonly string _connectionString;
public UserService(string connectionString)
{
_connectionString = connectionString;
}
private IDbConnection GetConnection() => new SqlConnection(_connectionString);
// ユーザー作成
public async Task<int> CreateUserAsync(User user)
{
const string sql = @"
INSERT INTO Users (Name, Email, Age, CreatedAt)
VALUES (@Name, @Email, @Age, @CreatedAt);
SELECT CAST(SCOPE_IDENTITY() as int);";
using var connection = GetConnection();
user.CreatedAt = DateTime.UtcNow;
var id = await connection.QuerySingleAsync<int>(sql, user);
return id;
}
// ユーザー取得(単一)
public async Task<User?> GetUserByIdAsync(int id)
{
const string sql = "SELECT * FROM Users WHERE Id = @id";
using var connection = GetConnection();
return await connection.QuerySingleOrDefaultAsync<User>(sql, new { id });
}
// ユーザー一覧取得
public async Task<IEnumerable<User>> GetAllUsersAsync()
{
const string sql = "SELECT * FROM Users ORDER BY CreatedAt DESC";
using var connection = GetConnection();
return await connection.QueryAsync<User>(sql);
}
// ユーザー更新
public async Task<bool> UpdateUserAsync(User user)
{
const string sql = @"
UPDATE Users
SET Name = @Name, Email = @Email, Age = @Age, UpdatedAt = @UpdatedAt
WHERE Id = @Id";
using var connection = GetConnection();
user.UpdatedAt = DateTime.UtcNow;
var rowsAffected = await connection.ExecuteAsync(sql, user);
return rowsAffected > 0;
}
// ユーザー削除
public async Task<bool> DeleteUserAsync(int id)
{
const string sql = "DELETE FROM Users WHERE Id = @id";
using var connection = GetConnection();
var rowsAffected = await connection.ExecuteAsync(sql, new { id });
return rowsAffected > 0;
}
// 条件付き検索
public async Task<IEnumerable<User>> GetUsersByAgeRangeAsync(int minAge, int maxAge)
{
const string sql = @"
SELECT * FROM Users
WHERE Age BETWEEN @minAge AND @maxAge
ORDER BY Age";
using var connection = GetConnection();
return await connection.QueryAsync<User>(sql, new { minAge, maxAge });
}
// パターンマッチング検索
public async Task<IEnumerable<User>> SearchUsersAsync(string searchTerm)
{
const string sql = @"
SELECT * FROM Users
WHERE Name LIKE @searchTerm OR Email LIKE @searchTerm
ORDER BY Name";
using var connection = GetConnection();
var parameter = new { searchTerm = $"%{searchTerm}%" };
return await connection.QueryAsync<User>(sql, parameter);
}
}
高度なクエリ操作
public class AdvancedUserOperations
{
private readonly string _connectionString;
public AdvancedUserOperations(string connectionString)
{
_connectionString = connectionString;
}
private IDbConnection GetConnection() => new SqlConnection(_connectionString);
// ページネーション
public async Task<(IEnumerable<User> Users, int TotalCount)> GetUsersPaginatedAsync(
int page, int pageSize, string? sortBy = "Id", bool ascending = true)
{
var sortDirection = ascending ? "ASC" : "DESC";
var offset = (page - 1) * pageSize;
var sql = $@"
SELECT * FROM Users
ORDER BY {sortBy} {sortDirection}
OFFSET @offset ROWS
FETCH NEXT @pageSize ROWS ONLY;
SELECT COUNT(*) FROM Users;";
using var connection = GetConnection();
using var multi = await connection.QueryMultipleAsync(sql, new { offset, pageSize });
var users = await multi.ReadAsync<User>();
var totalCount = await multi.ReadSingleAsync<int>();
return (users, totalCount);
}
// 動的クエリ構築
public async Task<IEnumerable<User>> SearchUsersWithFiltersAsync(UserSearchFilter filter)
{
var conditions = new List<string>();
var parameters = new DynamicParameters();
if (!string.IsNullOrEmpty(filter.Name))
{
conditions.Add("Name LIKE @name");
parameters.Add("name", $"%{filter.Name}%");
}
if (!string.IsNullOrEmpty(filter.Email))
{
conditions.Add("Email LIKE @email");
parameters.Add("email", $"%{filter.Email}%");
}
if (filter.MinAge.HasValue)
{
conditions.Add("Age >= @minAge");
parameters.Add("minAge", filter.MinAge.Value);
}
if (filter.MaxAge.HasValue)
{
conditions.Add("Age <= @maxAge");
parameters.Add("maxAge", filter.MaxAge.Value);
}
if (filter.CreatedAfter.HasValue)
{
conditions.Add("CreatedAt >= @createdAfter");
parameters.Add("createdAfter", filter.CreatedAfter.Value);
}
var whereClause = conditions.Any() ? "WHERE " + string.Join(" AND ", conditions) : "";
var sql = $"SELECT * FROM Users {whereClause} ORDER BY CreatedAt DESC";
using var connection = GetConnection();
return await connection.QueryAsync<User>(sql, parameters);
}
// 集約クエリ
public async Task<UserStatistics> GetUserStatisticsAsync()
{
const string sql = @"
SELECT
COUNT(*) as TotalUsers,
AVG(CAST(Age as FLOAT)) as AverageAge,
MIN(Age) as MinAge,
MAX(Age) as MaxAge,
COUNT(CASE WHEN Age < 30 THEN 1 END) as UsersUnder30,
COUNT(CASE WHEN Age BETWEEN 30 AND 50 THEN 1 END) as UsersBetween30And50,
COUNT(CASE WHEN Age > 50 THEN 1 END) as UsersOver50
FROM Users";
using var connection = GetConnection();
return await connection.QuerySingleAsync<UserStatistics>(sql);
}
// バルク操作
public async Task<int> BulkInsertUsersAsync(IEnumerable<User> users)
{
const string sql = @"
INSERT INTO Users (Name, Email, Age, CreatedAt)
VALUES (@Name, @Email, @Age, @CreatedAt)";
using var connection = GetConnection();
// 全ユーザーに作成日時を設定
var usersWithDate = users.Select(u =>
{
u.CreatedAt = DateTime.UtcNow;
return u;
});
return await connection.ExecuteAsync(sql, usersWithDate);
}
// トランザクション処理
public async Task<bool> TransferUserDataAsync(int sourceUserId, int targetUserId)
{
using var connection = GetConnection();
connection.Open();
using var transaction = connection.BeginTransaction();
try
{
// ソースユーザーのデータを取得
const string selectSql = "SELECT * FROM Users WHERE Id = @id";
var sourceUser = await connection.QuerySingleOrDefaultAsync<User>(
selectSql, new { id = sourceUserId }, transaction);
if (sourceUser == null)
return false;
// ターゲットユーザーを更新
const string updateSql = @"
UPDATE Users
SET Name = @Name, Email = @Email, UpdatedAt = @UpdatedAt
WHERE Id = @targetId";
await connection.ExecuteAsync(updateSql, new
{
sourceUser.Name,
sourceUser.Email,
UpdatedAt = DateTime.UtcNow,
targetId = targetUserId
}, transaction);
// ソースユーザーを削除
const string deleteSql = "DELETE FROM Users WHERE Id = @id";
await connection.ExecuteAsync(deleteSql, new { id = sourceUserId }, transaction);
// ログ記録
const string logSql = @"
INSERT INTO UserTransferLog (SourceUserId, TargetUserId, TransferredAt)
VALUES (@sourceUserId, @targetUserId, @transferredAt)";
await connection.ExecuteAsync(logSql, new
{
sourceUserId,
targetUserId,
transferredAt = DateTime.UtcNow
}, transaction);
transaction.Commit();
return true;
}
catch
{
transaction.Rollback();
throw;
}
}
}
// 検索フィルタークラス
public class UserSearchFilter
{
public string? Name { get; set; }
public string? Email { get; set; }
public int? MinAge { get; set; }
public int? MaxAge { get; set; }
public DateTime? CreatedAfter { get; set; }
}
// 統計データクラス
public class UserStatistics
{
public int TotalUsers { get; set; }
public double AverageAge { get; set; }
public int MinAge { get; set; }
public int MaxAge { get; set; }
public int UsersUnder30 { get; set; }
public int UsersBetween30And50 { get; set; }
public int UsersOver50 { get; set; }
}
リレーション操作
// 投稿モデル
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public int UserId { get; set; }
public bool IsPublished { get; set; }
public DateTime CreatedAt { get; set; }
// ナビゲーションプロパティ(Dapperでは手動管理)
public User? User { get; set; }
}
// ユーザーと投稿の複合オブジェクト
public class UserWithPosts
{
public User User { get; set; } = new();
public List<Post> Posts { get; set; } = new();
}
public class PostService
{
private readonly string _connectionString;
public PostService(string connectionString)
{
_connectionString = connectionString;
}
private IDbConnection GetConnection() => new SqlConnection(_connectionString);
// マルチマッピングを使用したユーザーと投稿の取得
public async Task<IEnumerable<Post>> GetPostsWithUsersAsync()
{
const string sql = @"
SELECT p.*, u.Id, u.Name, u.Email, u.Age, u.CreatedAt, u.UpdatedAt
FROM Posts p
INNER JOIN Users u ON p.UserId = u.Id
ORDER BY p.CreatedAt DESC";
using var connection = GetConnection();
return await connection.QueryAsync<Post, User, Post>(
sql,
(post, user) =>
{
post.User = user;
return post;
},
splitOn: "Id");
}
// 特定ユーザーの投稿一覧
public async Task<UserWithPosts?> GetUserWithPostsAsync(int userId)
{
const string sql = @"
SELECT * FROM Users WHERE Id = @userId;
SELECT * FROM Posts WHERE UserId = @userId ORDER BY CreatedAt DESC;";
using var connection = GetConnection();
using var multi = await connection.QueryMultipleAsync(sql, new { userId });
var user = await multi.ReadSingleOrDefaultAsync<User>();
if (user == null) return null;
var posts = (await multi.ReadAsync<Post>()).ToList();
return new UserWithPosts
{
User = user,
Posts = posts
};
}
// 階層データの取得(Dictionary使用)
public async Task<IEnumerable<UserWithPosts>> GetAllUsersWithPostsAsync()
{
const string sql = @"
SELECT u.*, p.Id as PostId, p.Title, p.Content, p.UserId, p.IsPublished, p.CreatedAt as PostCreatedAt
FROM Users u
LEFT JOIN Posts p ON u.Id = p.UserId
ORDER BY u.Id, p.CreatedAt DESC";
using var connection = GetConnection();
var userDictionary = new Dictionary<int, UserWithPosts>();
await connection.QueryAsync<User, Post, UserWithPosts>(
sql,
(user, post) =>
{
if (!userDictionary.TryGetValue(user.Id, out var userWithPosts))
{
userWithPosts = new UserWithPosts { User = user };
userDictionary[user.Id] = userWithPosts;
}
if (post != null)
{
userWithPosts.Posts.Add(post);
}
return userWithPosts;
},
splitOn: "PostId");
return userDictionary.Values;
}
// 投稿統計情報
public async Task<IEnumerable<UserPostStatistics>> GetUserPostStatisticsAsync()
{
const string sql = @"
SELECT
u.Id,
u.Name,
u.Email,
COUNT(p.Id) as TotalPosts,
COUNT(CASE WHEN p.IsPublished = 1 THEN 1 END) as PublishedPosts,
COUNT(CASE WHEN p.IsPublished = 0 THEN 1 END) as DraftPosts,
MAX(p.CreatedAt) as LastPostDate
FROM Users u
LEFT JOIN Posts p ON u.Id = p.UserId
GROUP BY u.Id, u.Name, u.Email
ORDER BY TotalPosts DESC";
using var connection = GetConnection();
return await connection.QueryAsync<UserPostStatistics>(sql);
}
// 複雑な検索(投稿内容でユーザー検索)
public async Task<IEnumerable<User>> FindUsersByPostContentAsync(string searchTerm)
{
const string sql = @"
SELECT DISTINCT u.*
FROM Users u
INNER JOIN Posts p ON u.Id = p.UserId
WHERE p.Title LIKE @searchTerm
OR p.Content LIKE @searchTerm
ORDER BY u.Name";
using var connection = GetConnection();
var parameter = new { searchTerm = $"%{searchTerm}%" };
return await connection.QueryAsync<User>(sql, parameter);
}
}
// 統計データ用のクラス
public class UserPostStatistics
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public int TotalPosts { get; set; }
public int PublishedPosts { get; set; }
public int DraftPosts { get; set; }
public DateTime? LastPostDate { get; set; }
}
実用例
// 実用的なアプリケーション例
public class BlogApplicationService
{
private readonly string _connectionString;
private readonly ILogger<BlogApplicationService> _logger;
public BlogApplicationService(string connectionString, ILogger<BlogApplicationService> logger)
{
_connectionString = connectionString;
_logger = logger;
}
private IDbConnection GetConnection() => new SqlConnection(_connectionString);
// ダッシュボードデータの取得
public async Task<DashboardData> GetDashboardDataAsync()
{
const string sql = @"
-- ユーザー統計
SELECT COUNT(*) as TotalUsers FROM Users;
-- 投稿統計
SELECT
COUNT(*) as TotalPosts,
COUNT(CASE WHEN IsPublished = 1 THEN 1 END) as PublishedPosts,
COUNT(CASE WHEN IsPublished = 0 THEN 1 END) as DraftPosts
FROM Posts;
-- 最新の投稿
SELECT TOP 5 p.*, u.Name as UserName
FROM Posts p
INNER JOIN Users u ON p.UserId = u.Id
WHERE p.IsPublished = 1
ORDER BY p.CreatedAt DESC;
-- 月別投稿数(過去12ヶ月)
SELECT
YEAR(CreatedAt) as Year,
MONTH(CreatedAt) as Month,
COUNT(*) as PostCount
FROM Posts
WHERE CreatedAt >= DATEADD(MONTH, -12, GETDATE())
GROUP BY YEAR(CreatedAt), MONTH(CreatedAt)
ORDER BY Year, Month;";
using var connection = GetConnection();
using var multi = await connection.QueryMultipleAsync(sql);
var userStats = await multi.ReadSingleAsync<dynamic>();
var postStats = await multi.ReadSingleAsync<dynamic>();
var recentPosts = await multi.ReadAsync<dynamic>();
var monthlyStats = await multi.ReadAsync<dynamic>();
return new DashboardData
{
TotalUsers = userStats.TotalUsers,
TotalPosts = postStats.TotalPosts,
PublishedPosts = postStats.PublishedPosts,
DraftPosts = postStats.DraftPosts,
RecentPosts = recentPosts.ToList(),
MonthlyPostCounts = monthlyStats.ToList()
};
}
// ストアドプロシージャの実行例
public async Task<IEnumerable<dynamic>> ExecuteReportProcedureAsync(DateTime startDate, DateTime endDate)
{
using var connection = GetConnection();
var parameters = new DynamicParameters();
parameters.Add("@StartDate", startDate);
parameters.Add("@EndDate", endDate);
parameters.Add("@TotalRecords", dbType: DbType.Int32, direction: ParameterDirection.Output);
var result = await connection.QueryAsync(
"sp_GenerateUserActivityReport",
parameters,
commandType: CommandType.StoredProcedure);
var totalRecords = parameters.Get<int>("@TotalRecords");
_logger.LogInformation("Report generated with {TotalRecords} records", totalRecords);
return result;
}
// 大量データの効率的な処理
public async Task ProcessLargeDatasetAsync(Func<User, Task> processor)
{
const string sql = "SELECT * FROM Users ORDER BY Id";
const int batchSize = 1000;
int offset = 0;
using var connection = GetConnection();
while (true)
{
var batchSql = $"{sql} OFFSET {offset} ROWS FETCH NEXT {batchSize} ROWS ONLY";
var users = await connection.QueryAsync<User>(batchSql);
if (!users.Any()) break;
var tasks = users.Select(processor);
await Task.WhenAll(tasks);
offset += batchSize;
_logger.LogInformation("Processed batch of {BatchSize} users, offset: {Offset}", batchSize, offset);
}
}
// エラーハンドリングとリトライ機能
public async Task<T?> ExecuteWithRetryAsync<T>(string sql, object? parameters = null, int maxRetries = 3)
{
for (int retry = 0; retry <= maxRetries; retry++)
{
try
{
using var connection = GetConnection();
return await connection.QuerySingleOrDefaultAsync<T>(sql, parameters);
}
catch (SqlException ex) when (retry < maxRetries && IsTransientError(ex))
{
var delay = TimeSpan.FromSeconds(Math.Pow(2, retry)); // Exponential backoff
_logger.LogWarning("Database operation failed (attempt {Retry}/{MaxRetries}). Retrying in {Delay}s. Error: {Error}",
retry + 1, maxRetries + 1, delay.TotalSeconds, ex.Message);
await Task.Delay(delay);
}
}
return default;
}
private static bool IsTransientError(SqlException ex)
{
// SQL Server の一時的なエラーコードをチェック
var transientErrorNumbers = new[] { 2, 53, 121, 233, 10053, 10054, 10060, 40197, 40501, 40613 };
return transientErrorNumbers.Contains(ex.Number);
}
// キャッシュ機能付きデータ取得
public async Task<IEnumerable<User>> GetCachedUsersAsync(IMemoryCache cache, TimeSpan? cacheExpiry = null)
{
const string cacheKey = "all_users";
if (cache.TryGetValue(cacheKey, out IEnumerable<User>? cachedUsers) && cachedUsers != null)
{
_logger.LogInformation("Returning cached user data");
return cachedUsers;
}
const string sql = "SELECT * FROM Users ORDER BY Name";
using var connection = GetConnection();
var users = await connection.QueryAsync<User>(sql);
var options = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = cacheExpiry ?? TimeSpan.FromMinutes(15)
};
cache.Set(cacheKey, users, options);
_logger.LogInformation("User data cached for {CacheExpiry} minutes", options.AbsoluteExpirationRelativeToNow?.TotalMinutes);
return users;
}
// パフォーマンス監視付きクエリ実行
public async Task<T> ExecuteWithPerformanceLoggingAsync<T>(string sql, object? parameters = null)
{
var stopwatch = Stopwatch.StartNew();
try
{
using var connection = GetConnection();
var result = await connection.QuerySingleAsync<T>(sql, parameters);
stopwatch.Stop();
_logger.LogInformation("Query executed in {ElapsedMs}ms: {Sql}",
stopwatch.ElapsedMilliseconds, sql.Substring(0, Math.Min(sql.Length, 100)));
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "Query failed after {ElapsedMs}ms: {Sql}",
stopwatch.ElapsedMilliseconds, sql.Substring(0, Math.Min(sql.Length, 100)));
throw;
}
}
}
// ダッシュボードデータクラス
public class DashboardData
{
public int TotalUsers { get; set; }
public int TotalPosts { get; set; }
public int PublishedPosts { get; set; }
public int DraftPosts { get; set; }
public List<dynamic> RecentPosts { get; set; } = new();
public List<dynamic> MonthlyPostCounts { get; set; } = new();
}
// 依存性注入でのサービス登録例(ASP.NET Core)
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<UserService>();
services.AddScoped<PostService>();
services.AddScoped<BlogApplicationService>();
services.AddScoped<AdvancedUserOperations>();
// 接続文字列の設定
services.AddScoped<string>(provider =>
provider.GetService<IConfiguration>()?.GetConnectionString("DefaultConnection") ?? "");
}
}