Stable schema
Tables and columns are shared and cannot immediately adopt domain terminology.
Architecture case study · open-source library
A CUSTOMER table exposes CUST_NO and LEGAL_NM; the domain should speak in Customer.Id and Customer.Name. FluentMap makes that translation explicit without persistence attributes on the entity.
The enterprise scenario is illustrative. It demonstrates public library contracts; it is not a customer, deployment, or production performance claim.
Context and motivation
Legacy schemas carry abbreviations, historical conventions, and names shared by systems that cannot all be renamed in one delivery. Repeating those names in the .NET model transfers persistence cost into business rules, APIs, and tests.
The goal is not to hide SQL or create a full ORM. Dapper remains responsible for queries and connections, while a centralized, strongly typed configuration translates physical names into properties that communicate intent.
Constraints and quality attributes
Tables and columns are shared and cannot immediately adopt domain terminology.
Entities receive no attributes that make them aware of persistence mechanics.
The 3.x line preserves primary 2.x APIs, with documented differences and opt-in additions.
Dapper compatibility, Core certification, and Dommel integration are separate claims.
Architecture decisions
EntityMap<T> connects property expressions to columns. Property renames go through the compiler, and configuration can be validated at startup.
Conventions reduce repetition when schemas follow patterns. Explicit maps remain suitable for exceptions and follow documented precedence.
Mapping precedence ↗Global configuration is built before concurrent queries and then treated as read-only. FluentMapRuntime provides isolated configuration for QueryMapped* APIs.
The Dommel package adds metadata and persistence integration, but its resolvers and builders remain process-global. Isolated FluentMap runtimes do not isolate Dommel.
Dommel integration ↗Source generators register eligible maps and materializers for supported shapes. They do not parse SQL or cover every materialization shape; unsupported cases can fall back.
Source generators ↗Analyzers detect configuration problems visible at compilation. Dynamic registration, reflection, and runtime-dependent behavior remain outside that proof.
Roslyn analyzers ↗Scenario implementation
using Dapper;
using Dapper.FluentMap;
using Dapper.FluentMap.Mapping;
FluentMapper.Initialize(config => config.AddMap<CustomerMap>());
FluentMapper.Validate();
const string sql = """
SELECT CUST_NO, LEGAL_NM
FROM CUSTOMER
WHERE CUST_NO = @id
""";
Customer customer = connection.QuerySingle<Customer>(sql, new { id = 42 });
public sealed class Customer
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
public sealed class CustomerMap : EntityMap<Customer>
{
public CustomerMap()
{
Map(customer => customer.Id).ToColumn("CUST_NO");
Map(customer => customer.Name).ToColumn("LEGAL_NM");
}
}
The example uses the current documented API. The application and Dapper still own the connection, transaction, parameters, query, and migrations.
The ADO.NET provider gives Dapper a reader containing CUST_NO and LEGAL_NM. FluentMap's installed type map resolves those names to Id and Name. Dapper creates and populates Customer; FluentMap neither tracks the entity nor rewrites the SQL.
Alternatives and trade-offs
| Approach | Example | Advantage | Cost and best fit |
|---|---|---|---|
| SQL aliases | SELECT CUST_NO AS Id, LEGAL_NM AS Name | No extra configuration; intent is visible in the query. | Repeats aliases across queries. Good for a one-off read or SQL owned by one operation. |
| Persistence DTO | CustomerRow.CUST_NO → Customer.Id | Fully explicit boundary and transformation; DTOs can model joins and projections. | More types and conversion code. Good when the persistence shape differs semantically from the aggregate. |
| Explicit FluentMap | Map(x => x.Id).ToColumn("CUST_NO") | Reuses a typed map and leaves the entity free of attributes. | Global configuration requires discipline and does not remove SQL. Good when entities recur across queries with stable physical names. |
Evolution and maintenance
EntityMap<T>, FluentMapper.Initialize(...), and the Dapper type-map bridge remain. For the common historical path, migration is usually a coordinated package update followed by validation and tests.
Normal Dapper.Query<T>() uses Dapper's global type map, and Dommel is also global. FluentMapRuntime isolation applies to mapped APIs that accept or belong to that runtime.
SQLite, SQL Server, PostgreSQL, MySQL, and MariaDB have real Core and Dommel evidence in the current matrix. Oracle and Firebird have Core evidence but no Dommel certification.
Compatibility matrix ↗The documented ranges are Dapper [2.1.79,3.0.0) and Dommel [3.5.3,4.0.0). Experimental canaries do not expand the matrix until it is intentionally updated and reviewed.
Limitations and risks
The library does not manage connections, transactions, migrations, tracking, or query design. Late global configuration can conflict with concurrent queries; initialize before use and then treat configuration as immutable.
Expected Dapper/ADO.NET compatibility does not equal FluentMap certification. Core certification does not imply Dommel. Full Native AOT support is also not claimed: the strict generated path has bounded coverage, and incompatible shapes can use runtime fallback.
Benchmarks use BenchmarkDotNet and SQLite in repository-defined scenarios. The committed baseline contains "reportOnly": true; it is exploratory evidence and a trend signal, not a general performance promise across environments, providers, or workloads.
Lessons
A map's value is not hiding the database but locating the translation. Aliases, DTOs, and FluentMap remain architectural options; reuse frequency, semantic distance, and isolation needs indicate which one costs less.
In library evolution, opt-in capabilities expand materialization, diagnostics, and integration without forcing historical consumers to adopt everything. Separate matrices and precise language keep test evidence from being generalized beyond its actual coverage.
Evidence and references