Nahmadov.DapperForge.SqlServer 2.3.0

dotnet add package Nahmadov.DapperForge.SqlServer --version 2.3.0
                    
NuGet\Install-Package Nahmadov.DapperForge.SqlServer -Version 2.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Nahmadov.DapperForge.SqlServer" Version="2.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Nahmadov.DapperForge.SqlServer" Version="2.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Nahmadov.DapperForge.SqlServer" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Nahmadov.DapperForge.SqlServer --version 2.3.0
                    
#r "nuget: Nahmadov.DapperForge.SqlServer, 2.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Nahmadov.DapperForge.SqlServer@2.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Nahmadov.DapperForge.SqlServer&version=2.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Nahmadov.DapperForge.SqlServer&version=2.3.0
                    
Install as a Cake Tool

Nahmadov.DapperForge

A lightweight, high-performance Dapper-based data access layer that provides an Entity Framework-style API surface while maintaining Dapper's speed and simplicity.


Key Philosophy

  • Immediate execution — No change tracker, no SaveChanges()
  • Explicit control — Direct SQL execution for maximum performance
  • Minimal allocations — Reduced memory pressure compared to EF Core
  • Database-agnostic — Pluggable SQL dialects (SQL Server, Oracle, SQLite)
  • Fluent API — Familiar EF-like query and configuration syntax

NuGet Packages

Package Description
Nahmadov.DapperForge.Core Core library with abstractions
Nahmadov.DapperForge.SqlServer SQL Server dialect
Nahmadov.DapperForge.Oracle Oracle dialect
Nahmadov.DapperForge.Sqlite SQLite dialect

Quick Start

Installation

dotnet add package Nahmadov.DapperForge.Core
dotnet add package Nahmadov.DapperForge.SqlServer

Define Your Context

public class AppDbContext : DapperDbContext
{
    public DapperSet<Customer> Customers => Set<Customer>();
    public DapperSet<Order> Orders => Set<Order>();

    public AppDbContext(DapperDbContextOptions<AppDbContext> options)
        : base(options) { }

    protected override void OnModelCreating(DapperModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Customer>(b =>
        {
            b.ToTable("Customers", "dbo");
            b.HasKey(c => c.Id);
            b.Property(c => c.Name).HasMaxLength(100).IsRequired();
        });
    }
}

Register in DI

services.AddDapperDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});

Use in Your Application

var activeCustomers = await _db.Customers
    .Where(c => c.IsActive)
    .OrderBy(c => c.Name)
    .ToListAsync();

var id = await _db.Customers.InsertAndGetIdAsync<int>(new Customer
{
    Name = "Alice",
    IsActive = true
});

await _db.Customers.DeleteByIdAsync(id);

Features

Fluent Query API

var customers = await db.Customers
    .Where(c => c.IsActive && c.Name.StartsWith("John"))
    .OrderBy(c => c.Name)
    .Skip(10)
    .Take(20)
    .ToListAsync();

Expression Translation

LINQ expressions are translated to parameterized SQL. Supported patterns:

Pattern Example Generated SQL
Comparisons c.Age > 18 WHERE a.[Age] > @Age
Null checks c.Email == null WHERE a.[Email] IS NULL
Booleans c.IsActive / !c.IsActive WHERE a.[IsActive] = 1 / = 0
String methods c.Name.StartsWith("J") WHERE a.[Name] LIKE @p0
Collection IN ids.Contains(c.Id) WHERE a.[Id] IN (1,2,3)
Logical ops &&, \|\|, ! AND, OR, NOT

Case-insensitive filtering is available via the ignoreCase parameter:

var customers = await db.Customers.WhereAsync(
    c => c.Name.Contains("john"), ignoreCase: true);
// WHERE LOWER(a.[Name]) LIKE LOWER(@p0)

Include / ThenInclude

Load related entities with an EF-style API:

var orders = await db.Orders
    .Include(o => o.Customer)
        .ThenInclude(c => c.Address)
    .Include(o => o.OrderItems)
        .ThenInclude(i => i.Product)
    .AsSplitQuery()
    .ToListAsync();

AsSingleQuery (default) — single SQL with JOINs; best for small result sets. AsSplitQuery — separate queries with IN clauses; avoids cartesian explosion for collections.

Bulk Insert

Insert large collections efficiently using automatic batching. Entities are split into batches that respect database parameter limits (2,100 for SQL Server, 32,767 for Oracle).

var products = GenerateProducts(5000);

var result = await db.Products.BulkInsertAsync(products);

Console.WriteLine($"Inserted {result.TotalAffected} rows in {result.BatchCount} batch(es), took {result.ElapsedTime}");

Configure with BulkInsertOptions:

var result = await db.Products.BulkInsertAsync(products, new BulkInsertOptions
{
    BatchSize = 200,
    CommandTimeout = 60,
    ValidateBeforeInsert = true   // default: true
});

Works with transactions:

var scope = await db.BeginTransactionScopeAsync();
try
{
    await db.Products.BulkInsertAsync(products, transaction: scope.Transaction);
    scope.Complete();
}
finally
{
    scope.Dispose();
}

Bulk Merge (Upsert)

Perform INSERT-or-UPDATE (MERGE) operations in bulk. Uses the SQL MERGE statement under the hood.

var result = await db.Products.BulkMergeAsync(products);

Console.WriteLine($"Affected {result.TotalAffected} rows (inserted: {result.RowsInserted}, updated: {result.RowsUpdated})");

Control merge behavior with BulkMergeOptions:

var result = await db.Products.BulkMergeAsync(products, new BulkMergeOptions
{
    MatchColumns = new[] { "TenantId", "ProductCode" },  // columns used for matching
    Mode = MergeMode.InsertOrUpdate,  // or InsertOnly, UpdateOnly
    BatchSize = 100,
    ValidateBeforeMerge = true
});

Shorthand for custom match columns:

var result = await db.Products.BulkMergeAsync(
    products,
    matchColumns: new[] { "TenantId", "ProductCode" });

Merge Modes:

Mode Behavior
InsertOrUpdate Insert new rows, update existing (default)
InsertOnly Only insert rows that don't exist
UpdateOnly Only update existing rows

Transactions

var scope = await db.BeginTransactionScopeAsync();
try
{
    var customerId = await db.Customers.InsertAndGetIdAsync<int>(customer, scope.Transaction);
    order.CustomerId = customerId;
    await db.Orders.InsertAsync(order, scope.Transaction);
    scope.Complete();
}
finally
{
    scope.Dispose();
}

Validation

Entities are validated before insert/update using [Required], [MaxLength], [StringLength] attributes or fluent configuration.

modelBuilder.Entity<Customer>(b =>
{
    b.Property(c => c.Name).IsRequired().HasMaxLength(100);
});

// Throws DapperValidationException on invalid data
await db.Customers.InsertAsync(customer);

Read-only entities and properties are protected from mutation.

Automatic Retry

Transient database failures (timeouts, deadlocks, connection drops) are automatically retried with exponential backoff.


Entity Configuration

Fluent API

public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
    public void Configure(EntityTypeBuilder<Customer> builder)
    {
        builder.ToTable("Customers", "dbo");
        builder.HasKey(c => c.Id);
        builder.Property(c => c.Name).HasColumnName("FullName").HasMaxLength(100).IsRequired();
        builder.Property(c => c.CreatedAt).IsReadOnly();
    }
}

Apply configurations:

protected override void OnModelCreating(DapperModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}

Data Annotations

[Table("Customers", Schema = "dbo")]
public class Customer
{
    [Key]
    public int Id { get; set; }

    [Required, MaxLength(100), Column("FullName")]
    public string Name { get; set; }

    [ReadOnly]
    public DateTime CreatedAt { get; set; }
}

Key Detection

Keys are resolved in order: [Key] attribute, fluent HasKey(), property named Id, or property named {TypeName}Id.


Database Dialects

Feature SQL Server Oracle
Identifier quoting [Table] "Table"
Parameter prefix @p0 :p0
Auto-generated keys SCOPE_IDENTITY() RETURNING ... INTO
Max parameters 2,100 32,767
Default schema dbo (none)
// SQL Server
options.UseSqlServer(connectionString);

// Oracle
options.UseOracle(connectionString);

CRUD API Reference

Operation Method
Get all GetAllAsync()
Find by ID FindAsync(id)
Filter WhereAsync(predicate) / .Where(pred).ToListAsync()
First FirstOrDefaultAsync(predicate)
Any AnyAsync(predicate)
Count CountAsync(predicate)
Insert InsertAsync(entity)
Insert + get ID InsertAndGetIdAsync<TKey>(entity)
Update UpdateAsync(entity)
Delete DeleteAsync(entity) / DeleteByIdAsync(id)
Bulk insert BulkInsertAsync(entities, options?)
Bulk merge BulkMergeAsync(entities, options?)

Architecture

Application Layer
       │
   DapperDbContext  (connection management, entity sets)
       │
  ┌────┼────┐
Query  Mutation  Config
  │    │         │
  SQL Generation Layer  (SqlGenerator, PredicateVisitor)
       │
  SQL Dialect Layer  (SqlServer, Oracle)
       │
  Dapper + ADO.NET

Design patterns: Repository (DapperSet<T>), Unit of Work (TransactionScope), Builder (model configuration), Strategy (ISqlDialect), Facade, Factory, Template Method.


Performance

  • No change tracker — minimal memory overhead
  • SQL statements pre-generated and cached
  • Expression compilation cached (thread-safe LRU, max 1,000 entries)
  • Identity cache prevents duplicate instances during Include operations
  • Parameterized queries enable database query plan caching
  • Bulk operations use automatic batching for optimal throughput

Migration from EF Core

API Equivalence

EF Core DapperForge
DbContext DapperDbContext
DbSet<T> DapperSet<T>
Where(pred).ToListAsync() WhereAsync(pred) or .Where(pred).ToListAsync()
Add() + SaveChangesAsync() InsertAsync()
Update() + SaveChangesAsync() UpdateAsync()
Remove() + SaveChangesAsync() DeleteAsync()
Include() / ThenInclude() Same API, same syntax
AsNoTracking() N/A — DapperForge never tracks
SaveChangesAsync() N/A — mutations execute immediately
IEntityTypeConfiguration<T> IEntityTypeConfiguration<T> (same interface)
modelBuilder.ApplyConfigurationsFromAssembly(...) Same method, same signature

Step 1 — Replace the Context Base Class

// EF Core
public class AppDbContext : DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
    protected override void OnModelCreating(ModelBuilder modelBuilder) { ... }
}

// DapperForge
public class AppDbContext : DapperDbContext
{
    public DapperSet<Customer> Customers => Set<Customer>();
    public AppDbContext(DapperDbContextOptions<AppDbContext> options) : base(options) { }
    protected override void OnModelCreating(DapperModelBuilder modelBuilder) { ... }
}

Step 2 — Register the Context

// EF Core
services.AddDbContext<AppDbContext>(o => o.UseSqlServer(connectionString));

// DapperForge
services.AddDapperDbContext<AppDbContext>(o => o.UseSqlServer(connectionString));

Both register the context as scoped — do not change the lifetime.

Step 3 — Convert Entity Configuration

EF Core IEntityTypeConfiguration<T> classes can be reused with minimal changes:

// EF Core
public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
    public void Configure(EntityTypeBuilder<Customer> builder)
    {
        builder.ToTable("Customers");
        builder.HasKey(c => c.Id);
        builder.Property(c => c.Name).HasColumnName("CustomerName");
    }
}

// DapperForge — identical interface, use DapperEntityTypeBuilder<T>
public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
    public void Configure(DapperEntityTypeBuilder<Customer> builder)
    {
        builder.ToTable("Customers");
        builder.HasKey(c => c.Id);
        builder.Property(c => c.Name).HasColumnName("CustomerName");
    }
}

Step 4 — Convert Query Patterns

// EF Core
var customers = await ctx.Customers
    .Where(c => c.IsActive)
    .OrderBy(c => c.Name)
    .Skip(20).Take(10)
    .ToListAsync();

// DapperForge — identical fluent API
var customers = await ctx.Customers
    .Query()
    .Where(c => c.IsActive)
    .OrderBy(c => c.Name)
    .Skip(20).Take(10)
    .ToListAsync();

Step 5 — Convert Mutations

// EF Core
ctx.Customers.Add(customer);
await ctx.SaveChangesAsync();

// DapperForge — immediate execution
await ctx.Customers.InsertAsync(customer);

Step 6 — Convert Transactions

// EF Core
await using var tx = await ctx.Database.BeginTransactionAsync();
try { ...; await ctx.SaveChangesAsync(); await tx.CommitAsync(); }
catch { await tx.RollbackAsync(); }

// DapperForge
var scope = await ctx.BeginTransactionScopeAsync();
try
{
    await ctx.Customers.InsertAsync(customer, scope.Transaction);
    await ctx.Orders.InsertAsync(order, scope.Transaction);
    scope.Complete();
}
finally { scope.Dispose(); }

Step 7 — Convert Include Queries

// EF Core
var orders = await ctx.Orders
    .Include(o => o.Customer)
    .Include(o => o.Lines)
    .AsNoTracking()
    .ToListAsync();

// DapperForge (single JOIN query — default)
var orders = await ctx.Orders
    .Query()
    .Include(o => o.Customer)
    .Include(o => o.Lines)
    .ToListAsync();

// DapperForge (split query — one SELECT per Include level)
var orders = await ctx.Orders
    .Query()
    .Include(o => o.Customer)
    .Include(o => o.Lines)
    .AsSplitQuery()
    .ToListAsync();

Known Gaps

EF Core feature DapperForge status
Lazy loading Not supported — use Include explicitly
Change tracking Not supported — DapperForge is always no-tracking
Migrations / DDL Not supported — manage schema externally
Composite primary keys Not supported — use HasAlternateKey as workaround
LINQ aggregations (Sum, GroupBy, …) Not supported — use raw SQL via QueryAsync
Complex projections (Select(x => new DTO{...})) Not supported — map manually after query

Limitations

  1. Single-column keys only — no composite key support
  2. Limited LINQ — no aggregations, grouping, or complex joins in the fluent API (use raw SQL)
  3. No lazy loading — must use Include
  4. No change tracking
  5. No migrations — DDL managed externally
  6. Expression translator covers common filter patterns only

Project Structure

DapperToolkit/
├── src/
│   ├── Nahmadov.DapperForge.Core/       # Core library
│   ├── Nahmadov.DapperForge.SqlServer/  # SQL Server dialect
│   ├── Nahmadov.DapperForge.Oracle/     # Oracle dialect
│   └── Nahmadov.DapperForge.Sqlite/     # SQLite dialect
├── tests/
│   └── Nahmadov.DapperForge.UnitTests/  # Unit tests
├── samples/
│   └── ConnectionSample/                # Usage samples
└── docs/                                # Detailed documentation

Testing

dotnet test

Sample App

cd samples/ConnectionSample
dotnet run

Configure via user secrets (FullSample):

  • Provider: SqlServer (default) or Oracle
  • SqlServerConnectionString / OracleConnectionString

Documentation

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.3.0 35 3/16/2026
2.2.0 98 2/9/2026
2.1.3 101 1/16/2026
2.1.2 96 1/16/2026
2.1.1 100 1/15/2026
2.1.0 100 1/14/2026
2.0.0 99 1/7/2026
1.1.0 98 1/7/2026
1.0.1 445 12/9/2025

Unreleased:
- Core: unified SqlPredicateTranslator; AsSplitQuery Include filter bug fixed.

v2.2.0:
- BulkInsertAsync and BulkMergeAsync (uses SQL Server MERGE statement).
- Include/ThenInclude Where filter support.