FclEx.Dapper
2.4.2
dotnet add package FclEx.Dapper --version 2.4.2
NuGet\Install-Package FclEx.Dapper -Version 2.4.2
<PackageReference Include="FclEx.Dapper" Version="2.4.2" />
<PackageVersion Include="FclEx.Dapper" Version="2.4.2" />
<PackageReference Include="FclEx.Dapper" />
paket add FclEx.Dapper --version 2.4.2
#r "nuget: FclEx.Dapper, 2.4.2"
#:package FclEx.Dapper@2.4.2
#addin nuget:?package=FclEx.Dapper&version=2.4.2
#tool nuget:?package=FclEx.Dapper&version=2.4.2
FclEx.Dapper
FclEx.Dapper adds focused, cache-aware CRUD and transaction helpers to Dapper while keeping connections, transactions, SQL providers, and execution boundaries visible to the caller. It is not an ORM: it does not provide change tracking, relationship management, LINQ translation, schema migrations, repositories, or a unit of work.
Installation
Install FclEx.Dapper and the ADO.NET provider used by the application:
dotnet add package FclEx.Dapper
dotnet add package Microsoft.Data.Sqlite
The package targets net472, netstandard2.0, net8.0, net9.0, and net10.0. It references Dapper and FclEx.Core, but it does not add a database provider dependency.
Quick Start
Map an entity with DataAnnotations:
[Table("widgets")]
public sealed class Widget
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public string Name { get; set; } = "";
}
Use a normal provider connection. The schema must already exist; the CREATE TABLE below is included only to make the example runnable:
using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
await connection.ExecuteAsync(
"CREATE TABLE widgets (Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL)");
var id = await connection.InsertAsync(new Widget { Name = "first" });
var widget = await connection.GetAsync<Widget>(id);
await connection.BulkInsertAsync(
[
new Widget { Name = "second" },
new Widget { Name = "third" },
]);
await connection.DeleteAsync<Widget>(id);
InsertAsync<TEntity> returns the generated key as long. Use InsertAsync<TEntity, TKey> when the generated key has another type.
Providers and SQL Adapters
FclEx.Dapper recognizes these provider connection types when the corresponding provider package is installed:
| Database | Provider package | Built-in adapter | Generated-key SQL |
|---|---|---|---|
| SQL Server | Microsoft.Data.SqlClient |
SqlServerAdapter |
OUTPUT INSERTED |
| PostgreSQL | Npgsql |
NpgsqlAdapter |
RETURNING |
| Oracle | Oracle.ManagedDataAccess / Oracle.ManagedDataAccess.Core |
OracleAdapter |
RETURNING ... INTO |
| SQLite | Microsoft.Data.Sqlite |
SqliteAdapter |
last_insert_rowid() |
| MySQL | MySql.Data |
MySqlAdapter |
LAST_INSERT_ID() |
| MySQL/MariaDB | MySqlConnector |
MySqlConnectorAdapter |
LAST_INSERT_ID() |
For generated Oracle insert commands, string values without a store type name use OracleDbType.NVarchar2. Explicit store types such as VARCHAR2, NVARCHAR2, CLOB, and NCLOB retain their configured parameter types. This parameter default does not create or change database columns and does not affect ordinary Dapper calls.
For generated Npgsql commands, DateTimeOffset parameters are converted to UTC when no store type is supplied or the explicit type is TimestampTz, timestamptz, or timestamp with time zone. This preserves the instant; PostgreSQL does not retain the original offset. Timestamp / timestamp without time zone and other explicit types leave values unchanged and may reject incompatible values. The adapter uses parameter mapping, does not inspect database columns, and does not convert DateTime values. This behavior does not affect ordinary Dapper calls.
Oracle uses double-quoted, case-sensitive identifiers and : parameter placeholders. Map identifiers with their actual database casing (typically uppercase for objects created without quotes). BulkInsertAsync uses multi-row VALUES, requiring Oracle Database 23ai or later. The adapter limits each command to floor(65535 / parameterCountPerRow) rows; the shared insert pipeline also caps each batch at 500 rows. Oracle restricts identity/sequence defaults in multi-row VALUES; supply explicit keys where the column permits them, or insert such rows individually. Single-row inserts return numeric, string, and date/time generated keys using RETURNING ... INTO output parameters. Default-only inserts are supported when a single generated key is requested, using that column with DEFAULT; otherwise an explicit insertable column is required. Explicit identity values require a column defined as GENERATED BY DEFAULT; GENERATED ALWAYS columns reject explicit values.
Adapter resolution examines the runtime connection type and its base types. Register an adapter for a custom or wrapped connection type when it cannot be recognized automatically:
DapperHelper.RegisterSqlAdapter<CustomConnection>(new CustomSqlAdapter()); // add or replace
var added = DapperHelper.TryRegisterSqlAdapter<CustomConnection>(new CustomSqlAdapter());
TryRegisterSqlAdapter returns false when the exact connection type is already registered. A registered adapter must keep its SQL-affecting behavior stable because generated SQL is cached by adapter instance. Schema support follows ISqlAdapter.SupportsSchemas; unsupported adapters ignore mapped and per-call schemas.
Table, schema, and column names supplied through mappings or method arguments must come from trusted application configuration, not end-user input. Adapters quote and escape identifiers, but identifier names cannot be parameterized like data values.
Entity Mapping and Key Limits
DataAnnotationsEntityMappingSource is used by default. It supports:
[Table], includingSchema[Column], including column aliases and provider store type names[Key][NotMapped][DatabaseGenerated]withNone,Identity, orComputed
By convention, public readable and writable scalar instance properties are persistent. Static properties, indexers, read-only properties, navigation properties, and explicitly unmapped properties are excluded. A non-scalar property must declare an explicit mapping attribute to be included.
GetAsync<T> and DeleteAsync<T> require exactly one mapped key. Generated-key return from InsertAsync requires exactly one generated key. Composite-key lookup and deletion are not supported.
Implement IEntityMappingSource when mappings should be independent of DataAnnotations. A source must return the same immutable EntityMapping instance whenever the same entity type is requested because mapping identity participates in SQL cache keys:
var mapping = new EntityMapping(
typeof(Widget),
"widgets",
[
new(typeof(Widget).GetProperty(nameof(Widget.Id))!, "Id", true,
DatabaseValueGeneration.OnInsert),
new(typeof(Widget).GetProperty(nameof(Widget.Name))!, "Name"),
]);
var options = new CommandOptions { EntityMappingSource = new ApplicationMappingSource(mapping) };
var id = await connection.InsertAsync(new Widget { Name = "mapped" }, commandOptions: options);
ApplicationMappingSource in this example is an application-owned IEntityMappingSource implementation.
Commands, Connections, and Transactions
CommandOptions carries the command timeout, local transaction, adapter override, and mapping source. The same options shape is accepted by connection and transaction CRUD methods. Cancellation is supplied separately through the final optional cancellationToken parameter, which covers connection opening and command execution.
await connection.BulkInsertAsync(
entities,
commandOptions: options,
cancellationToken: cancellationToken);
Migration note: CommandOptions.CancellationToken has been removed. Pass the token directly to each asynchronous CRUD method instead. This is a breaking API change; consumers must rebuild, and calls that initialized the removed property must be updated.
CRUD helpers record the connection's initial state. A connection opened by the helper is closed before the operation returns; a connection supplied already open remains open. Transaction extension methods bind the receiver transaction to the generated command.
ExecuteInTransactionAsync starts a local transaction with ReadCommitted by default, commits after a successful callback, and attempts rollback after callback or commit failure. If both the operation and rollback fail, an AggregateException contains both exceptions.
Bulk Inserts and SQL Caching
BulkInsertAsync emits bounded multi-row INSERT commands; it does not silently execute one command per entity. A batch contains at most 500 rows and may be smaller because of provider row or parameter limits. Multiple rows with no insertable properties are rejected when the adapter cannot express an efficient bulk form.
NpgsqlAdapter converts scalar ushort, uint, and ulong parameter values to int, long, and decimal, respectively. These conversions preserve their full ranges and allow PostgreSQL integer, bigint, and numeric parameters to be inferred. byte values remain unchanged. The conversion applies to generated insert commands, including bulk inserts; it does not change ordinary Dapper calls or configure table column types. Explicit PostgreSQL unsigned types (oid, xid, xid8, cid, regtype, and regconfig) retain their original values. Other explicit store types still determine the parameter type and must accept the supplied value.
Canonical CRUD command text is cached by adapter instance, immutable mapping identity, operation shape, and batch row count. Per-call schema and adapter overrides do not enter the process-wide cache. This avoids repeated SQL string construction without permanently retaining open-ended override values.
Explicit Generated Keys
Use InsertWithExplicitGeneratedKeysAsync or BulkInsertAsync(..., includeAutoKey: true) only when importing values for keys normally generated by the database.
These operations do not advance or reset provider identity, sequence, or auto-increment state. The caller must maintain that state so later database-generated keys do not conflict with explicitly inserted values.
Table Existence Checks
Use TableExistsAsync to query a table by name or entity mapping:
bool exists = await connection.TableExistsAsync("Orders", schema: "Sales", cancellationToken: cancellationToken);
bool mappedTableExists = await connection.TableExistsAsync<Order>(commandOptions: options, cancellationToken: cancellationToken);
Names are unquoted components passed as parameters, rather than interpolated SQL. The entity overload honors its mapping source and mapped schema; an explicit schema overrides the mapping. Command options support a local transaction, timeout, and adapter override. The operation restores a connection it opened to Closed and preserves an already-open connection.
SQL Server and PostgreSQL use native unqualified-name resolution when no schema is supplied. Oracle uses CURRENT_SCHEMA and exact catalog casing. MySqlConnector treats schema as a database name; MySql.Data ignores it and checks the selected database. SQLite searches main and temp with case-insensitive name comparison and ignores schema arguments. SQLite attached databases and SQL Server temporary tables are outside this lookup. Views and synonyms are excluded. A false result reflects catalog visibility for the current login; database access failures propagate. The check does not reserve or lock the table against concurrent DDL.
ISqlAdapter.BuildTableExistsCommandText supplies the metadata query. Direct interface implementations must implement this new member. SqlAdapterBase preserves existing derived implementations with a default that throws NotSupportedException until metadata querying is supplied.
Identity Sequence Synchronization
Use this maintenance operation after importing explicit keys or deleting high keys:
bool synchronized = await connection.ReseedIdentityAsync<Order>(
schema: "Sales", commandOptions: options, cancellationToken: cancellationToken);
await connection.ReseedIdentityAsync("Orders", "OrderId", schema: "Sales");
The entity overload requires exactly one mapped database-generated integer key and uses its physical column
name. The result is false when the table or native identity generator is absent. Invalid mappings and database
access errors throw. A connection opened by this operation is closed before return.
| Database | Native operation | Behavior |
|---|---|---|
| PostgreSQL 10+ | pg_get_serial_sequence and setval |
Next key is MAX + the positive sequence increment; empty tables restart at the sequence start. |
| SQL Server | DBCC CHECKIDENT |
Reseeds an ascending identity to MAX; an empty, previously used identity restarts at its seed. |
| MySQL / MariaDB | ALTER TABLE ... AUTO_INCREMENT |
Requests MAX + 1, or 1 when empty; the engine may retain a higher counter and session increment/offset still apply. |
| SQLite | sqlite_sequence maintenance |
Resets AUTOINCREMENT to MAX, or zero when empty, in the supplied or a locally owned transaction; ordinary ROWID returns false. Temp shadows main; attached databases are excluded. |
| Oracle 12.2+ | ALTER TABLE ... START WITH LIMIT VALUE |
Uses native identity limit calculation and retains generation mode and options. Trigger-managed sequences are excluded. |
Pause concurrent writes and other users of the sequence during synchronization. This API does not coordinate
writers, and resetting a counter may reuse deleted values. PostgreSQL sequence changes survive rollback;
MySQL and Oracle DDL implicitly commit. Namespace rules match CRUD, including MySql.Data and SQLite ignoring
schema arguments. Custom ISqlAdapter implementations must implement ReseedIdentityAsync;
SqlAdapterBase throws NotSupportedException by default. This is a source-breaking interface addition.
Table Truncation
TruncateAsync accepts an unquoted table name or an entity mapping and removes every row in the physical table:
await connection.TruncateAsync<Order>(schema: "Sales", commandOptions: options, cancellationToken: cancellationToken);
await connection.TruncateAsync("Orders", restartIdentity: true, cascade: false,
schema: "Sales", commandOptions: options, cancellationToken: cancellationToken);
The entity overload honors its mapping source and mapped schema; an explicit schema overrides the mapping. No key is required. Identifier components are escaped by the resolved or overridden SQL adapter. The helper preserves the connection's initial state and accepts local transactions, timeouts, and cancellation through the existing command options. It does not validate EF-style inheritance or table sharing and does not consult application filters, soft-delete rules, or entity state.
| Database | Default identity behavior | Explicit options |
|---|---|---|
| SQL Server / MySQL | Reset | restartIdentity: true, cascade: false |
| PostgreSQL | Preserve | All combinations |
| Oracle | Preserve | restartIdentity: false; CASCADE requires Oracle 12c+ and ON DELETE CASCADE foreign keys |
| SQLite | Reset AUTOINCREMENT when present | cascade: false; reset or preserve AUTOINCREMENT |
SQLite uses DELETE followed by sequence maintenance in one transaction. It preserves caller-owned
transactions and otherwise creates and commits a local transaction. Main and temp tables are supported;
attached databases are excluded. DELETE triggers and foreign-key actions execute. Ordinary ROWID allocation
follows SQLite rules regardless of restartIdentity. Native TRUNCATE behavior is database-dependent:
foreign keys can prevent execution, CASCADE can affect additional tables, and MySQL/Oracle implicitly commit.
Adapters provide BuildTruncateCommandText and ExecuteTruncateAsync. Direct ISqlAdapter implementations
must implement these new members. SqlAdapterBase rejects unsupported truncation by default and provides
ordinary command execution; SQLite overrides execution for atomic sequence maintenance.
Dapper Global State and Type Handlers
On first use, DapperHelper calls Initialize() to register Dapper.GuidTypeHandler and Dapper.DateTimeOffsetTypeHandler independently when no application handler is registered for that type. Installing a default handler removes the corresponding parameter type maps, including nullable and application mappings, so Dapper uses that handler for parameters. Applications registering their own handlers remain responsible for removing conflicting parameter maps. These changes affect all Dapper calls in the process. Call Initialize() explicitly before ordinary Dapper operations or after resetting Dapper's handlers. Core CRUD operations do not scan assemblies or change other Dapper settings. Generated queries alias database columns back to CLR property names, so they do not require a global Dapper type map.
DateTimeOffsetTypeHandler reads native DateTimeOffset values, invariant text (including SQLite TEXT), and DateTime values. Explicit offsets and available tick precision are preserved. Unspecified date-time values and text without an offset are interpreted as UTC; local date-time values retain their instant. Nullable scalar results preserve database nulls. Parameters retain their supplied offset and use DbType.DateTimeOffset; the provider determines whether that representation is supported. This handler does not change the destination column type or recover offsets discarded by a database.
Dapper.AssumeUtcDateTimeTypeHandler remains opt-in. Registering it through SqlMapper.AddTypeHandler changes Dapper process-wide state and remains the application's responsibility.
See DESIGN.md for the principles governing future changes.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 is compatible. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.7.2
- Dapper (>= 2.1.89)
- FclEx.Core (>= 2.4.2)
-
.NETStandard 2.0
- Dapper (>= 2.1.89)
- FclEx.Core (>= 2.4.2)
-
net10.0
- Dapper (>= 2.1.89)
- FclEx.Core (>= 2.4.2)
-
net8.0
- Dapper (>= 2.1.89)
- FclEx.Core (>= 2.4.2)
-
net9.0
- Dapper (>= 2.1.89)
- FclEx.Core (>= 2.4.2)
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.4.2 | 122 | 10/10/2026 |
| 2.4.1 | 100 | 10/8/2026 |
| 2.4.0 | 110 | 10/3/2026 |
| 2.3.9 | 105 | 9/27/2026 |
| 2.3.8 | 103 | 9/26/2026 |
| 2.3.7 | 97 | 9/24/2026 |
| 2.3.6 | 114 | 9/22/2026 |
| 2.3.5 | 105 | 9/22/2026 |
| 2.3.4 | 109 | 8/28/2026 |
| 2.3.3 | 122 | 8/22/2026 |
| 2.3.2 | 124 | 8/20/2026 |
| 2.3.1 | 114 | 8/19/2026 |
| 2.3.0 | 130 | 8/17/2026 |
| 2.2.9 | 131 | 7/18/2026 |
| 2.2.8 | 137 | 6/13/2026 |
| 2.2.6 | 131 | 6/8/2026 |
| 2.2.5 | 137 | 6/5/2026 |
| 2.2.4 | 136 | 6/5/2026 |
| 2.2.3 | 137 | 5/27/2026 |
| 2.2.2 | 132 | 5/26/2026 |