Linq2Db
Overview
The Linq2Db provider wires Linq2DbRepository<TEntity> into the repository abstraction layer. It implements IReadOnlyRepository<TEntity>, IWriteOnlyRepository<TEntity>, and ILinqRepository<TEntity>, giving you LINQ query support and paging without requiring a full ORM like EF Core.
Linq2Db operates through a RCommonDataConnection, which wraps Linq2Db's DataConnection and implements IDataStore so the IDataStoreFactory can resolve named connections at runtime. Unlike EF Core, Linq2Db has no server-side change tracking, so IGraphRepository<TEntity> (with its Tracking property) is not supported.
Installation
dotnet add package RCommon.Linq2DbData connection setup
Your connection class must derive from RCommonDataConnection:
using RCommon.Persistence.Linq2Db;
public class AppDataConnection : RCommonDataConnection
{
public AppDataConnection(DataOptions options)
: base(options)
{
}
}
RCommonDataConnection inherits from Linq2Db's DataConnection, so you can configure entity mappings on it using Linq2Db's fluent mapping API.
Configuration
Register the Linq2Db persistence provider in your application startup:
builder.Services.AddRCommon()
.WithPersistence<Linq2DbPersistenceBuilder>(linq2db =>
{
linq2db.AddDataConnection<AppDataConnection>(
"AppDb",
(serviceProvider, dataOptions) =>
dataOptions.UseSqlServer(
builder.Configuration.GetConnectionString("AppDb")));
linq2db.SetDefaultDataStore(ds =>
ds.DefaultDataStoreName = "AppDb");
});
Multiple connections can be registered by calling AddDataConnection more than once with different names.
Database provider options
The options factory receives the current DataOptions and returns a configured instance. Linq2Db supports all major databases:
// SQL Server
dataOptions.UseSqlServer(connectionString)
// PostgreSQL
dataOptions.UsePostgreSQL(connectionString)
// SQLite
dataOptions.UseSQLite(connectionString)
// MySQL
dataOptions.UseMySQL(connectionString)
Usage
Injecting and targeting a data store
Inject ILinqRepository<TEntity> (or a narrower interface) and set DataStoreName:
public class ProductQueryHandler
{
private readonly ILinqRepository<Product> _products;
public ProductQueryHandler(ILinqRepository<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
ICollection<Product> available =
await _repo.FindAsync(p => p.StockQuantity > 0, cancellationToken);
// Single or default
Product? featured =
await _repo.FindSingleOrDefaultAsync(p => p.IsFeatured, cancellationToken);
// Existence
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
int affected = await _repo.DeleteManyAsync(p => p.StockQuantity == 0, cancellationToken);
LINQ queries
ILinqRepository<TEntity> exposes IQueryable<TEntity> directly and provides FindQuery overloads that return IQueryable:
IQueryable<Product> query = _repo.FindQuery(p => p.CategoryId == categoryId);
// Further compose with LINQ operators
var names = await query.Select(p => p.Name).ToListAsync();
Paged queries
IPaginatedList<Product> page = await _repo.FindAsync(
expression: p => p.CategoryId == categoryId,
orderByExpression: p => p.Name,
orderByAscending: true,
pageNumber: 1,
pageSize: 20,
token: cancellationToken);
Or use a PagedSpecification<TEntity>:
var spec = new PagedSpecification<Product>(
p => p.CategoryId == categoryId,
p => p.Name,
orderByAscending: true,
pageNumber: 1,
pageSize: 20);
IPaginatedList<Product> page = await _repo.FindAsync(spec, cancellationToken);
Eager loading
var orders = await _orderRepo
.Include(o => o.Customer)
.FindAsync(o => o.Status == OrderStatus.Pending, cancellationToken);
Linq2Db implements eager loading using LoadWith / ThenLoad internally, which translates to SQL JOIN statements.
Using specifications
var spec = new ActiveProductSpec();
ICollection<Product> active = await _repo.FindAsync(spec, cancellationToken);
Soft delete
If the entity implements ISoftDelete, delete operations mark IsDeleted = true and issue an UPDATE rather than a physical DELETE. To bypass:
await _repo.DeleteAsync(product, isSoftDelete: false, cancellationToken);
Primary keys
Unlike the Dapper provider (which infers a database-generated key from the "Id" naming convention), LinqToDB never guesses — an auto-increment key must be mapped explicitly, either with an attribute on the entity or with fluent mapping at startup. Without this mapping, AddAsync inserts the entity's key exactly as set (0 for a new int key), so a second insert fails with a primary-key violation.
Fluent mapping is the more convenient option for entities that derive from BusinessEntity<TKey>, since it doesn't require redeclaring the inherited Id property:
using LinqToDB.Mapping;
new FluentMappingBuilder(MappingSchema.Default)
.Entity<Product>().HasTableName("Products")
.Property(p => p.Id).IsPrimaryKey().IsIdentity()
.Build();
Register this once at startup, before the first DataConnection is built. A Guid or string key needs no such mapping — it is always inserted exactly as you set it.
See Examples/Persistence/Examples.Persistence.Linq2Db/ for a complete console app exercising this end to end.
API Summary
| Type | Purpose |
|---|---|
ILinq2DbPersistenceBuilder | Fluent startup builder with AddDataConnection<TDataConnection> and SetDefaultDataStore |
Linq2DbPersistenceBuilder | Concrete implementation that registers Linq2Db repositories in the DI container |
RCommonDataConnection | Abstract base class for all Linq2Db connections; derives from DataConnection, implements IDataStore |
Linq2DbRepository<TEntity> | Concrete repository; implements ILinqRepository<TEntity>, IReadOnlyRepository<TEntity>, IWriteOnlyRepository<TEntity> |
ILinqRepository<TEntity> | Full CRUD + IQueryable<TEntity> + paging + eager loading |