Dapper
Overview
The Dapper provider wires DapperRepository<TEntity> into the repository abstraction layer. It implements ISqlMapperRepository<TEntity>, IReadOnlyRepository<TEntity>, and IWriteOnlyRepository<TEntity>.
Under the hood the Dapper provider uses Dommel for SQL generation. Dommel translates entity mappings and LINQ-style lambda expressions into parameterized SQL, so common CRUD operations do not require hand-written SQL. Raw SQL execution is still available through ISqlMapperRepository<TEntity> when you need it.
Unlike the EF Core provider, Dapper does not expose ILinqRepository<TEntity> or IGraphRepository<TEntity> because it has no change-tracking mechanism. Each operation opens a DbConnection from the configured RDbConnection, executes the statement, and closes the connection.
Installation
dotnet add package RCommon.DapperConnection setup
Your connection class must derive from RDbConnection, which implements IDataStore so that the IDataStoreFactory can resolve it by name.
using Microsoft.Extensions.Options;
using RCommon.Persistence.Sql;
public class AppDbConnection : RDbConnection
{
public AppDbConnection(IOptions<RDbConnectionOptions> options)
: base(options)
{
}
}
RDbConnectionOptions carries the connection string. Configure it when registering the connection (see below).
Configuration
Register the Dapper persistence provider in your application startup:
builder.Services.AddRCommon()
.WithPersistence<DapperPersistenceBuilder>(dapper =>
{
dapper.AddDbConnection<AppDbConnection>(
"AppDb",
options => options.ConnectionString =
builder.Configuration.GetConnectionString("AppDb"));
dapper.SetDefaultDataStore(ds =>
ds.DefaultDataStoreName = "AppDb");
});
Multiple connections can be registered by calling AddDbConnection more than once with different names.
Usage
Injecting and targeting a data store
Inject ISqlMapperRepository<TEntity> (or the narrower IReadOnlyRepository<TEntity> / IWriteOnlyRepository<TEntity>) and set DataStoreName:
public class ProductQueryHandler
{
private readonly IReadOnlyRepository<Product> _products;
public ProductQueryHandler(IReadOnlyRepository<Product> products)
{
_products = products;
_products.DataStoreName = "AppDb";
}
}
CRUD operations
// Create
await _repo.AddAsync(product, cancellationToken);
await _repo.AddRangeAsync(products, cancellationToken);
// Read by primary key
Product? p = await _repo.FindAsync(productId, cancellationToken);
// Read by expression (Dommel translates to SQL WHERE)
ICollection<Product> available =
await _repo.FindAsync(p => p.StockQuantity > 0, cancellationToken);
// Single or default
Product? featured =
await _repo.FindSingleOrDefaultAsync(p => p.IsFeatured, cancellationToken);
// Existence check
bool exists = await _repo.AnyAsync(p => p.Sku == sku, cancellationToken);
// Count
long count = await _repo.GetCountAsync(p => p.CategoryId == categoryId, cancellationToken);
// Update
await _repo.UpdateAsync(product, cancellationToken);
// Delete (auto-detects ISoftDelete)
await _repo.DeleteAsync(product, cancellationToken);
// Bulk delete by expression
int affected = await _repo.DeleteManyAsync(p => p.StockQuantity == 0, cancellationToken);
Primary keys
AddAsync/AddRangeAsync populate an auto-increment int/long Id with the database-generated value after insert — inject ISqlMapperRepository<TEntity> (or IWriteOnlyRepository<TEntity>), call AddAsync, and the entity's Id is set when the call returns, the same as with the EF Core provider. A Guid or string Id is always inserted exactly as you set it — the Dapper provider never overwrites a non-numeric key.
See Examples/Persistence/Examples.Persistence.Dapper/ for a complete console app exercising this end to end, including the SQLite-specific DommelMapper.AddSqlBuilder registration needed for identity retrieval against SQLite.
Soft delete
If the entity implements ISoftDelete, DeleteAsync and DeleteManyAsync will set IsDeleted = true and issue an UPDATE rather than a physical DELETE. To force a physical DELETE regardless of the interface:
await _repo.DeleteAsync(product, isSoftDelete: false, cancellationToken);
Using specifications
Specifications work the same as with any other provider:
var spec = new ActiveProductSpec();
ICollection<Product> active = await _repo.FindAsync(spec, cancellationToken);
Raw SQL via ISqlMapperRepository
ISqlMapperRepository<TEntity> exposes QueryAsync and ExecuteAsync for scenarios where Dommel's SQL generation is insufficient:
public class ReportService
{
private readonly ISqlMapperRepository<SalesReport> _reports;
public ReportService(ISqlMapperRepository<SalesReport> reports)
{
_reports = reports;
_reports.DataStoreName = "AppDb";
}
public async Task<IEnumerable<SalesReport>> GetMonthlyAsync(int year, int month,
CancellationToken cancellationToken)
{
return await _reports.QueryAsync(
"SELECT * FROM SalesReports WHERE Year = @Year AND Month = @Month",
new { Year = year, Month = month },
cancellationToken);
}
}
Dommel entity mapping
Dommel infers table and column names from entity class and property names by convention. You can configure mappings explicitly:
DommelMapper.SetTableNameResolver(new PluralizedTableNameResolver());
DommelMapper.AddSqlBuilder(typeof(SqlConnection), new SqlServerSqlBuilder());
Refer to the Dommel documentation for the full mapping API.
API Summary
| Type | Purpose |
|---|---|
IDapperBuilder | Fluent startup builder with AddDbConnection<TDbConnection> and SetDefaultDataStore |
DapperPersistenceBuilder | Concrete implementation that registers Dapper repositories in the DI container |
RDbConnection | Abstract base class for all Dapper connections; implements IDataStore |
RDbConnectionOptions | Options carrying the connection string |
DapperRepository<TEntity> | Concrete repository; implements ISqlMapperRepository<TEntity>, IReadOnlyRepository<TEntity>, IWriteOnlyRepository<TEntity> |
ISqlMapperRepository<TEntity> | Extends read/write with raw-SQL QueryAsync and ExecuteAsync |