After six years without a new stable release, Dapper FluentMap is actively maintained again. But bringing an existing library back to life involves more than updating dependencies. It means finding a way to move forward without breaking the contracts that existing applications rely on.
Imagine a .NET application that uses Dapper to query a legacy database. In the code, we have a Customer entity with properties such as Id and Name. In the database, the columns follow a different naming convention: customer_id and customer_name.
Handling this difference is not particularly difficult. We can use SQL aliases, introduce intermediate objects, or configure explicit mappings. All are reasonable approaches, depending on the context.
The problem becomes more noticeable when these differences spread across dozens of queries and entities. Naming conventions, legacy database structures, and persistence-specific decisions gradually find their way into the application code.
Dapper FluentMap was originally created to address this kind of problem: mapping .NET properties to database columns without introducing persistence attributes into domain entities.
In August 2020, version 2.0.0 was released, updating its integration with Dapper 2.0.35 and Dommel 2.0. After that, the project went through a long period without new stable releases.
In 2026, I took over its maintenance.
The first question wasn't how many features could be added. It was how to evolve an established library without disrupting applications that might still depend on it.
Today, with version 3.6.0, released on October 13, 2026, FluentMap retains its original purpose while introducing capabilities that were not available in the 2.x line.
The First Decision: Preserve What Already Worked
A general-purpose library carries a different kind of responsibility than an application maintained by a single team.
When we change an application, we usually know its consumers and can coordinate a migration. With a public library, we don't necessarily know how many projects depend on a particular behavior or which architectural decisions were made around it.
For that reason, I see compatibility as an architectural requirement, not merely a versioning concern.
In FluentMap, the traditional configuration model remains available:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
public sealed class CustomerMap : EntityMap<Customer>
{
public CustomerMap()
{
Map(x => x.Id).ToColumn("customer_id");
Map(x => x.Name).ToColumn("customer_name");
}
}
Registration remains familiar:
FluentMapper.Initialize(config =>
{
config.AddMap<CustomerMap>();
});
FluentMapper.Validate();
And queries still use Dapper directly:
var customer = connection.QuerySingle<Customer>(
"SELECT customer_id, customer_name FROM customers WHERE customer_id = @Id",
new { Id = 10 });
FluentMap establishes the relationship between column names and entity properties. SQL execution, connection management, and basic materialization remain Dapper's responsibility.
Internally, this integration relies on Dapper's type-mapping mechanism, connected to FluentMap's global configuration.
That behavior has been preserved in the 3.x line.
For applications using only traditional mappings, upgrading generally does not require rewriting entities or replacing existing query calls. This does not mean unrestricted compatibility: dependency versions and integration scenarios still need to be validated.
The evolution also does not turn FluentMap into a full ORM. It does not introduce entity tracking, migration management, or generic CRUD generation in the core package.
The goal remains to complement Dapper, not replace it.
When Property Mapping Is No Longer Enough
Mapping a database column to a property solves many problems, but domain models are not always made up of simple scalar properties.
In applications following Domain-Driven Design, for example, value objects, nested properties, and immutable entities are common. There are also situations where the same entity needs to be materialized from queries with different result shapes.
FluentMap 3.x introduces specific mechanisms for these scenarios.
An entity can now map a nested property:
public sealed class CustomerMap : EntityMap<Customer>
{
public CustomerMap()
{
Map(x => x.Address.City).ToColumn("city");
}
}
To materialize that path, we use one of FluentMap's dedicated query APIs:
var customer = connection.QueryMappedSingle<Customer>(
"SELECT 'Sao Paulo' AS city");
This distinction matters.
Registering a nested mapping does not automatically make every standard Dapper query capable of constructing complex object structures. The functionality depends on using the appropriate materialization path.
Beyond nested objects, the 3.x line introduces mapping profiles, property converters, and explicit construction strategies through ConstructUsing(...).
Mapping profiles allow an entity to support different query result shapes. Property converters handle transformations between database values and model properties. Explicit construction provides an alternative when automatic constructor selection does not meet an entity's requirements.
These features expand the available modeling options, but they also introduce decisions that developers need to make deliberately.
A Dapper TypeHandler<T>, for example, remains a good choice when a type has a consistent scalar representation throughout an application. There is little reason to replace it with a property-specific converter if the existing mechanism already solves the problem correctly.
This is a principle I try to preserve: a new capability does not automatically make an existing approach obsolete.
Global Configuration and Isolation: Two Execution Models
Another important part of FluentMap's evolution concerns configuration.
Historically, FluentMapper.Initialize(...) established a global configuration. This model is straightforward and remains suitable for applications with a single set of mappings.
However, global configuration requires attention in parallel tests, applications with multiple contexts, and scenarios where the same entity may need different mapping definitions.
The 3.x line introduces immutable configuration and isolated FluentMapRuntime instances:
var runtime = new FluentMapConfigurationBuilder()
.AddMap<CustomerMap>()
.Build()
.CreateRuntime();
var customer = runtime.QueryMappedSingle<Customer>(
connection,
sql);
In this model, the configuration belongs to the runtime instance used to execute the query.
A separate package, FluentMap.DependencyInjection, also provides integration with .NET's dependency injection container.
But there is an important boundary: isolation applies to materialization APIs controlled by FluentMap.
Standard Dapper.Query<T>() calls continue to use Dapper's globally registered type maps, while Dommel integration remains associated with global configuration.
In other words, introducing dependency injection does not automatically eliminate global state from an application.
Choosing between these models depends on the need for isolation. For a straightforward application, keeping the traditional configuration may result in lower maintenance overhead. For scenarios requiring independent mapping configurations, an isolated runtime provides greater control.
Moving Some Work from Runtime to Compile Time
One of the most interesting changes was introducing capabilities that operate during compilation.
FluentMap now provides two additional packages: FluentMap.Analyzers and FluentMap.Generators.
Roslyn Analyzers identify certain configuration problems before the application runs. This helps detect mistakes that might otherwise surface only during initialization or query materialization.
Source Generators provide generated mapping registration and materializers for supported scenarios.
The architectural difference is that some work can move from runtime mechanisms into the compilation process, reducing dependence on reflection in those paths.
This approach also made strict generated materialization possible. Instead of silently falling back to reflection-based materialization, strict mode rejects unsupported result shapes and configurations.
There are potential benefits in predictability, diagnostics, and reducing reflection usage. However, it would be premature to claim that generated code makes every query faster.
Performance depends on result shapes, data volumes, ADO.NET providers, and application characteristics. Improvements should be assessed through representative benchmarks, not assumed simply because code generation is involved.
Similarly, although the project includes tests for specific Native AOT scenarios, version 3.6.0 does not claim full Native AOT compatibility. Reflection-based registration and other dynamic paths still have documented limitations.
Dommel and the Meaning of Persistence Operations
The integration with Dommel has evolved as well.
In the historical version, Ignore() could end up being used as a workaround to prevent certain properties from being written to the database.
However, excluding a property from mapping is not the same as declaring that it can be read but should not participate in an INSERT or UPDATE.
The current version distinguishes these cases through dedicated persistence metadata:
Map(x => x.CreatedAt)
.ToColumn("created_at")
.DatabaseDefaultOnInsert();
Map(x => x.UpdatedAt)
.ToColumn("updated_at")
.ReadOnly();
Map(x => x.Total)
.ToColumn("total")
.Computed();
It is now possible to express whether a column has a database-generated default, is computed, or must be excluded from write operations.
Version 3.5.0 also introduced InsertMapped* and UpdateMapped* operations, which can apply property converters during persistence.
These operations are opt-in and do not automatically change the behavior of traditional Dommel calls.
For applications migrating from earlier versions, this is an area that deserves particular attention. A query returning the expected data does not prove that inserts and updates still behave correctly.
Compatibility Needs Evidence, Not Assumptions
A recurring challenge with libraries built on ADO.NET is defining what compatibility with a database actually means.
The fact that Dapper works with a particular provider does not automatically mean that every additional feature offered by another library has been validated against that provider.
In version 3.6.0, FluentMap introduced documented compatibility levels supported by mandatory integration tests against real database instances.
SQLite, SQL Server, PostgreSQL, MySQL, and MariaDB have certified coverage for both FluentMap Core and Dommel integration.
Oracle and Firebird have Core certification, but not Dommel certification.
SQL Server Compact Edition remains classified as a legacy integration without modern certification.
This separation helps prevent an expectation of compatibility from becoming a guarantee that testing cannot support.
The compatibility matrix also identifies the specific database and provider versions used during validation.
The current packages declare support for Dapper versions starting at 2.1.79 and below 3.0.0. For Dommel, the supported range starts at 3.5.3 and remains below 4.0.0.
Distributed packages continue to target .NET Standard 2.0, preserving compatibility with a broad range of consuming applications.
Beyond functional tests, project maintenance now includes public API compatibility checks, static analysis, code coverage, release artifacts, and automated compatibility validation.
I don't consider these secondary activities.
For an open-source library, the release process and the ability to detect regressions are part of the reliability we provide to consumers.
Should You Upgrade from Version 2.0?
For applications still using FluentMap 2.0.0, my recommendation is to start with existing behavior rather than the new features.
First, identify how mappings are registered, which Dapper and Dommel versions are installed, and whether any integrations depend on specific behaviors.
Then update FluentMap packages to the same release version, build the application, validate mappings, and run the test suite.
If Dommel is involved, pay particular attention to write operations, generated keys, database defaults, and ignored properties. Configuration combinations previously accepted may also produce validation errors in the newer version.
Binary compatibility deserves attention too. The Dommel integration has differences in its public API, so full binary compatibility with the historical 2.0.0 package should not be assumed.
Only after validating the existing application would I consider adopting features such as isolated runtimes, advanced materialization, or Source Generators.
A successful migration does not require using every available feature.
In many cases, the best decision is simply to upgrade the library while leaving most application code unchanged.
What Bringing FluentMap Back Means
Between versions 2.0.0 and 3.6.0, FluentMap went from a library whose maintenance had effectively stopped to a project with active development, new capabilities, documentation, compatibility testing, and an established release process.
But the most important aspect, in my view, is not the number of features added.
It is the ability to keep using a familiar abstraction without forcing existing applications to change how they work, while giving new projects access to mechanisms better suited to their requirements.
Maintaining open-source software means balancing two responsibilities that often pull in different directions: preserving what consumers already depend on and allowing the project to evolve.
For FluentMap, the approach has been to preserve the historical API as the compatibility path and introduce most new behaviors as optional capabilities.
There are still limitations, particularly around global integration isolation and Native AOT support. They are part of the current contract and should remain explicit.
To me, this is one of the most important measures of a library's evolution: not simply offering more possibilities, but helping consumers understand its contracts, limitations, and the consequences of their choices.
FluentMap is actively maintained again. And for developers working with Dapper, version 3.6.0 offers new options without requiring them to rewrite what already works.
Top comments (0)