Skip to main content
Version: 2.4.1

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​

NuGet Package
dotnet add package RCommon.Dapper

Connection setup​

Your connection class must derive from RDbConnection, which implements IDataStore so that the IDataStoreFactory can resolve it by name.

using RCommon.Persistence.Sql;

public class AppDbConnection : RDbConnection
{
public AppDbConnection(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);

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​

TypePurpose
IDapperBuilderFluent startup builder with AddDbConnection<TDbConnection> and SetDefaultDataStore
DapperPersistenceBuilderConcrete implementation that registers Dapper repositories in the DI container
RDbConnectionAbstract base class for all Dapper connections; implements IDataStore
RDbConnectionOptionsOptions 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
RCommonRCommon