Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Build .NET backend services with clean architecture, dependency injection, Entity Framework, and ASP.NET Core patterns.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/dapper-patterns.md
1# Dapper Patterns and Best Practices23Advanced patterns for high-performance data access with Dapper in .NET.45## Why Dapper?67| Aspect | Dapper | EF Core |8| ---------------- | ------------------------------ | ---------------------- |9| Performance | ~10x faster for simple queries | Good with optimization |10| Control | Full SQL control | Abstracted |11| Learning curve | Low (just SQL) | Higher |12| Complex mappings | Manual | Automatic |13| Change tracking | None | Built-in |14| Migrations | External tools | Built-in |1516**Use Dapper when:**1718- Performance is critical (hot paths)19- You need complex SQL (CTEs, window functions)20- Read-heavy workloads21- Legacy database schemas2223**Use EF Core when:**2425- Rich domain models with relationships26- Need change tracking27- Want LINQ-to-SQL translation28- Complex object graphs2930## Connection Management3132### 1. Proper Connection Handling3334```csharp35// Register connection factory36services.AddScoped<IDbConnection>(sp =>37{38var connectionString = sp.GetRequiredService<IConfiguration>()39.GetConnectionString("Default");40return new SqlConnection(connectionString);41});4243// Or use a factory for more control44public interface IDbConnectionFactory45{46IDbConnection CreateConnection();47}4849public class SqlConnectionFactory : IDbConnectionFactory50{51private readonly string _connectionString;5253public SqlConnectionFactory(IConfiguration configuration)54{55_connectionString = configuration.GetConnectionString("Default")56?? throw new InvalidOperationException("Connection string not found");57}5859public IDbConnection CreateConnection() => new SqlConnection(_connectionString);60}61```6263### 2. Connection Lifecycle6465```csharp66public class ProductRepository67{68private readonly IDbConnectionFactory _factory;6970public ProductRepository(IDbConnectionFactory factory)71{72_factory = factory;73}7475public async Task<Product?> GetByIdAsync(string id, CancellationToken ct)76{77// Connection opens automatically, closes on dispose78using var connection = _factory.CreateConnection();7980return await connection.QueryFirstOrDefaultAsync<Product>(81new CommandDefinition(82"SELECT * FROM Products WHERE Id = @Id",83new { Id = id },84cancellationToken: ct));85}86}87```8889## Query Patterns9091### 3. Basic CRUD Operations9293```csharp94// SELECT single95var product = await connection.QueryFirstOrDefaultAsync<Product>(96"SELECT * FROM Products WHERE Id = @Id",97new { Id = id });9899// SELECT multiple100var products = await connection.QueryAsync<Product>(101"SELECT * FROM Products WHERE CategoryId = @CategoryId",102new { CategoryId = categoryId });103104// INSERT with identity return105var newId = await connection.QuerySingleAsync<int>(106"""107INSERT INTO Products (Name, Price, CategoryId)108VALUES (@Name, @Price, @CategoryId);109SELECT CAST(SCOPE_IDENTITY() AS INT);110""",111product);112113// INSERT with OUTPUT clause (returns full entity)114var inserted = await connection.QuerySingleAsync<Product>(115"""116INSERT INTO Products (Name, Price, CategoryId)117OUTPUT INSERTED.*118VALUES (@Name, @Price, @CategoryId);119""",120product);121122// UPDATE123var rowsAffected = await connection.ExecuteAsync(124"""125UPDATE Products126SET Name = @Name, Price = @Price, UpdatedAt = @UpdatedAt127WHERE Id = @Id128""",129new { product.Id, product.Name, product.Price, UpdatedAt = DateTime.UtcNow });130131// DELETE132await connection.ExecuteAsync(133"DELETE FROM Products WHERE Id = @Id",134new { Id = id });135```136137### 4. Dynamic Query Building138139```csharp140public async Task<IReadOnlyList<Product>> SearchAsync(ProductSearchCriteria criteria)141{142var sql = new StringBuilder("SELECT * FROM Products WHERE 1=1");143var parameters = new DynamicParameters();144145if (!string.IsNullOrWhiteSpace(criteria.SearchTerm))146{147sql.Append(" AND (Name LIKE @SearchTerm OR Sku LIKE @SearchTerm)");148parameters.Add("SearchTerm", $"%{criteria.SearchTerm}%");149}150151if (criteria.CategoryId.HasValue)152{153sql.Append(" AND CategoryId = @CategoryId");154parameters.Add("CategoryId", criteria.CategoryId.Value);155}156157if (criteria.MinPrice.HasValue)158{159sql.Append(" AND Price >= @MinPrice");160parameters.Add("MinPrice", criteria.MinPrice.Value);161}162163if (criteria.MaxPrice.HasValue)164{165sql.Append(" AND Price <= @MaxPrice");166parameters.Add("MaxPrice", criteria.MaxPrice.Value);167}168169// Pagination170sql.Append(" ORDER BY Name");171sql.Append(" OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY");172parameters.Add("Offset", (criteria.Page - 1) * criteria.PageSize);173parameters.Add("PageSize", criteria.PageSize);174175using var connection = _factory.CreateConnection();176var results = await connection.QueryAsync<Product>(sql.ToString(), parameters);177return results.ToList();178}179```180181### 5. Multi-Mapping (Joins)182183```csharp184// One-to-One mapping185public async Task<Product?> GetProductWithCategoryAsync(string id)186{187const string sql = """188SELECT p.*, c.*189FROM Products p190INNER JOIN Categories c ON p.CategoryId = c.Id191WHERE p.Id = @Id192""";193194using var connection = _factory.CreateConnection();195196var result = await connection.QueryAsync<Product, Category, Product>(197sql,198(product, category) =>199{200product.Category = category;201return product;202},203new { Id = id },204splitOn: "Id"); // Column where split occurs205206return result.FirstOrDefault();207}208209// One-to-Many mapping210public async Task<Order?> GetOrderWithItemsAsync(int orderId)211{212const string sql = """213SELECT o.*, oi.*, p.*214FROM Orders o215LEFT JOIN OrderItems oi ON o.Id = oi.OrderId216LEFT JOIN Products p ON oi.ProductId = p.Id217WHERE o.Id = @OrderId218""";219220var orderDictionary = new Dictionary<int, Order>();221222using var connection = _factory.CreateConnection();223224await connection.QueryAsync<Order, OrderItem, Product, Order>(225sql,226(order, item, product) =>227{228if (!orderDictionary.TryGetValue(order.Id, out var existingOrder))229{230existingOrder = order;231existingOrder.Items = new List<OrderItem>();232orderDictionary.Add(order.Id, existingOrder);233}234235if (item != null)236{237item.Product = product;238existingOrder.Items.Add(item);239}240241return existingOrder;242},243new { OrderId = orderId },244splitOn: "Id,Id");245246return orderDictionary.Values.FirstOrDefault();247}248```249250### 6. Multiple Result Sets251252```csharp253public async Task<(IReadOnlyList<Product> Products, int TotalCount)> SearchWithCountAsync(254ProductSearchCriteria criteria)255{256const string sql = """257-- First result set: count258SELECT COUNT(*) FROM Products WHERE CategoryId = @CategoryId;259260-- Second result set: data261SELECT * FROM Products262WHERE CategoryId = @CategoryId263ORDER BY Name264OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY;265""";266267using var connection = _factory.CreateConnection();268using var multi = await connection.QueryMultipleAsync(sql, new269{270CategoryId = criteria.CategoryId,271Offset = (criteria.Page - 1) * criteria.PageSize,272PageSize = criteria.PageSize273});274275var totalCount = await multi.ReadSingleAsync<int>();276var products = (await multi.ReadAsync<Product>()).ToList();277278return (products, totalCount);279}280```281282## Advanced Patterns283284### 7. Table-Valued Parameters (Bulk Operations)285286```csharp287// SQL Server TVP for bulk operations288public async Task<IReadOnlyList<Product>> GetByIdsAsync(IEnumerable<string> ids)289{290// Create DataTable matching TVP structure291var table = new DataTable();292table.Columns.Add("Id", typeof(string));293294foreach (var id in ids)295{296table.Rows.Add(id);297}298299using var connection = _factory.CreateConnection();300301var results = await connection.QueryAsync<Product>(302"SELECT p.* FROM Products p INNER JOIN @Ids i ON p.Id = i.Id",303new { Ids = table.AsTableValuedParameter("dbo.StringIdList") });304305return results.ToList();306}307308// SQL to create the TVP type:309// CREATE TYPE dbo.StringIdList AS TABLE (Id NVARCHAR(40));310```311312### 8. Stored Procedures313314```csharp315public async Task<IReadOnlyList<Product>> GetTopProductsAsync(int categoryId, int count)316{317using var connection = _factory.CreateConnection();318319var results = await connection.QueryAsync<Product>(320"dbo.GetTopProductsByCategory",321new { CategoryId = categoryId, TopN = count },322commandType: CommandType.StoredProcedure);323324return results.ToList();325}326327// With output parameters328public async Task<(Order Order, string ConfirmationCode)> CreateOrderAsync(Order order)329{330var parameters = new DynamicParameters(new331{332order.CustomerId,333order.Total334});335parameters.Add("OrderId", dbType: DbType.Int32, direction: ParameterDirection.Output);336parameters.Add("ConfirmationCode", dbType: DbType.String, size: 20, direction: ParameterDirection.Output);337338using var connection = _factory.CreateConnection();339340await connection.ExecuteAsync(341"dbo.CreateOrder",342parameters,343commandType: CommandType.StoredProcedure);344345order.Id = parameters.Get<int>("OrderId");346var confirmationCode = parameters.Get<string>("ConfirmationCode");347348return (order, confirmationCode);349}350```351352### 9. Transactions353354```csharp355public async Task<Order> CreateOrderWithItemsAsync(Order order, List<OrderItem> items)356{357using var connection = _factory.CreateConnection();358await connection.OpenAsync();359360using var transaction = await connection.BeginTransactionAsync();361362try363{364// Insert order365order.Id = await connection.QuerySingleAsync<int>(366"""367INSERT INTO Orders (CustomerId, Total, CreatedAt)368OUTPUT INSERTED.Id369VALUES (@CustomerId, @Total, @CreatedAt)370""",371order,372transaction);373374// Insert items375foreach (var item in items)376{377item.OrderId = order.Id;378}379380await connection.ExecuteAsync(381"""382INSERT INTO OrderItems (OrderId, ProductId, Quantity, UnitPrice)383VALUES (@OrderId, @ProductId, @Quantity, @UnitPrice)384""",385items,386transaction);387388await transaction.CommitAsync();389390order.Items = items;391return order;392}393catch394{395await transaction.RollbackAsync();396throw;397}398}399```400401### 10. Custom Type Handlers402403```csharp404// Register custom type handler for JSON columns405public class JsonTypeHandler<T> : SqlMapper.TypeHandler<T>406{407public override T Parse(object value)408{409if (value is string json)410{411return JsonSerializer.Deserialize<T>(json)!;412}413return default!;414}415416public override void SetValue(IDbDataParameter parameter, T value)417{418parameter.Value = JsonSerializer.Serialize(value);419parameter.DbType = DbType.String;420}421}422423// Register at startup424SqlMapper.AddTypeHandler(new JsonTypeHandler<ProductMetadata>());425426// Now you can query directly427var product = await connection.QueryFirstAsync<Product>(428"SELECT Id, Name, Metadata FROM Products WHERE Id = @Id",429new { Id = id });430// product.Metadata is automatically deserialized from JSON431```432433## Performance Tips434435### 11. Use CommandDefinition for Cancellation436437```csharp438// Always use CommandDefinition for async operations439var result = await connection.QueryAsync<Product>(440new CommandDefinition(441commandText: "SELECT * FROM Products WHERE CategoryId = @CategoryId",442parameters: new { CategoryId = categoryId },443cancellationToken: ct,444commandTimeout: 30));445```446447### 12. Buffered vs Unbuffered Queries448449```csharp450// Buffered (default) - loads all results into memory451var products = await connection.QueryAsync<Product>(sql); // Returns list452453// Unbuffered - streams results (lower memory for large result sets)454var products = await connection.QueryUnbufferedAsync<Product>(sql); // Returns IAsyncEnumerable455456await foreach (var product in products)457{458// Process one at a time459}460```461462### 13. Connection Pooling Settings463464```json465{466"ConnectionStrings": {467"Default": "Server=localhost;Database=MyDb;User Id=sa;Password=xxx;TrustServerCertificate=True;Min Pool Size=5;Max Pool Size=100;Connection Timeout=30;"468}469}470```471472## Common Patterns473474### Repository Base Class475476```csharp477public abstract class DapperRepositoryBase<T> where T : class478{479protected readonly IDbConnectionFactory ConnectionFactory;480protected readonly ILogger Logger;481protected abstract string TableName { get; }482483protected DapperRepositoryBase(IDbConnectionFactory factory, ILogger logger)484{485ConnectionFactory = factory;486Logger = logger;487}488489protected async Task<T?> GetByIdAsync<TId>(TId id, CancellationToken ct = default)490{491var sql = $"SELECT * FROM {TableName} WHERE Id = @Id";492493using var connection = ConnectionFactory.CreateConnection();494return await connection.QueryFirstOrDefaultAsync<T>(495new CommandDefinition(sql, new { Id = id }, cancellationToken: ct));496}497498protected async Task<IReadOnlyList<T>> GetAllAsync(CancellationToken ct = default)499{500var sql = $"SELECT * FROM {TableName}";501502using var connection = ConnectionFactory.CreateConnection();503var results = await connection.QueryAsync<T>(504new CommandDefinition(sql, cancellationToken: ct));505506return results.ToList();507}508509protected async Task<int> ExecuteAsync(510string sql,511object? parameters = null,512CancellationToken ct = default)513{514using var connection = ConnectionFactory.CreateConnection();515return await connection.ExecuteAsync(516new CommandDefinition(sql, parameters, cancellationToken: ct));517}518}519```520521## Anti-Patterns to Avoid522523```csharp524// ❌ Bad - SQL injection risk525var sql = $"SELECT * FROM Products WHERE Name = '{userInput}'";526527// ✅ Good - Parameterized query528var sql = "SELECT * FROM Products WHERE Name = @Name";529await connection.QueryAsync<Product>(sql, new { Name = userInput });530531// ❌ Bad - Not disposing connection532var connection = new SqlConnection(connectionString);533var result = await connection.QueryAsync<Product>(sql);534// Connection leak!535536// ✅ Good - Using statement537using var connection = new SqlConnection(connectionString);538var result = await connection.QueryAsync<Product>(sql);539540// ❌ Bad - Opening connection manually when not needed541await connection.OpenAsync(); // Dapper does this automatically542var result = await connection.QueryAsync<Product>(sql);543544// ✅ Good - Let Dapper manage connection545var result = await connection.QueryAsync<Product>(sql);546```547