Estudo de caso arquitetural · biblioteca open source

Evoluindo o acesso a dados sem acoplar o domínio ao banco legado

Uma tabela CUSTOMER expõe CUST_NO e LEGAL_NM; o domínio precisa falar em Customer.Id e Customer.Name. O FluentMap torna essa tradução explícita sem colocar atributos de persistência na entidade.

O cenário corporativo é ilustrativo. Ele demonstra contratos públicos da biblioteca; não representa cliente, implantação ou ganho de desempenho em produção.

Contexto e motivação

O vocabulário do banco não precisa se tornar o vocabulário do domínio.

Schemas legados carregam abreviações, convenções históricas e nomes compartilhados por sistemas que não podem ser renomeados em uma única entrega. Repetir esses nomes no modelo .NET transfere o custo de persistência para regras de negócio, APIs e testes.

O objetivo não é esconder SQL nem criar um ORM completo. É manter a consulta e a conexão sob responsabilidade do Dapper, enquanto uma configuração tipada e centralizada traduz os nomes físicos para propriedades que comunicam intenção.

Restrições e atributos de qualidade

Modernizar a borda sem quebrar contratos históricos.

01

Schema estável

Tabelas e colunas são compartilhadas e não podem seguir imediatamente a nomenclatura do domínio.

02

Domínio limpo

Entidades não recebem atributos que as façam conhecer o mecanismo de persistência.

03

Compatibilidade

A linha 3.x preserva as principais APIs históricas da 2.x, com diferenças documentadas e recursos novos opt-in.

04

Evidência por camada

Compatibilidade do Dapper, certificação do Core e integração Dommel são afirmações distintas.

ManutenibilidadeTestabilidadeCompatibilidadeLegibilidadeIsolamentoEvolutibilidade
Fluxo da tabela CUSTOMER com CUST_NO e LEGAL_NM, passando pelo Dapper e pelo EntityMap, até Customer com Id e Name.
O resultado continua vindo do SQL executado pelo Dapper; o map define como as colunas materializam o objeto.

Decisões arquiteturais

Mapeamento é uma fronteira explícita, não uma abstração sobre todo o acesso a dados.

Mapas tipados

EntityMap<T> associa expressões de propriedade a colunas. Renomear uma propriedade passa pelo compilador e a configuração pode ser validada no startup.

Mapeamento de propriedades ↗

Convenções com precedência

Convenções reduzem repetição quando o schema segue padrões. Mapas explícitos continuam adequados para exceções e têm precedência documentada.

Precedência ↗

Configuração imutável

A configuração global é construída antes das consultas concorrentes e tratada como somente leitura. FluentMapRuntime permite configurações isoladas nas APIs QueryMapped*.

Isolamento em runtime ↗

Dommel opcional

O pacote Dommel adiciona metadados e persistência, mas mantém estado global nos resolvers e builders. Runtimes isolados do FluentMap não isolam Dommel.

Integração Dommel ↗

Geração opt-in

Source generators registram maps elegíveis e materializadores para formas suportadas. Eles não analisam SQL nem garantem toda forma de materialização; há fallback em casos não suportados.

Source generators ↗

Diagnósticos estáticos

Analyzers detectam configurações observáveis em compilação. Registro dinâmico, reflexão e comportamento dependente de runtime permanecem fora dessa prova.

Analyzers ↗

Implementação do cenário

Uma entidade sem atributos, um map na infraestrutura e uma consulta Dapper normal.

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");
    }
}

O exemplo usa a API atual documentada. A conexão, a transação, a parametrização, a consulta e migrations continuam sob responsabilidade da aplicação e do Dapper.

Fluxo de materialização

O provider ADO.NET entrega ao Dapper um reader com CUST_NO e LEGAL_NM. O type map instalado pelo FluentMap resolve esses nomes para Id e Name. O Dapper cria e preenche Customer; o FluentMap não rastreia a entidade nem muda o SQL.

Alternativas e trade-offs

Três abordagens válidas, com custos em lugares diferentes.

AbordagemExemploVantagemCusto e melhor contexto
Aliases no SQLSELECT CUST_NO AS Id, LEGAL_NM AS NameSem configuração adicional; intenção visível na consulta.Repete aliases em várias consultas. Boa para leitura pontual ou SQL inteiramente controlado por uma única operação.
DTO de persistênciaCustomerRow.CUST_NO → Customer.IdFronteira e transformação totalmente explícitas; DTO pode refletir joins e projeções.Mais tipos e código de conversão. Boa quando o shape de persistência difere semanticamente do agregado.
FluentMap explícitoMap(x => x.Id).ToColumn("CUST_NO")Reutiliza um map tipado e mantém a entidade livre de atributos.Configuração global exige disciplina; não elimina SQL. Boa quando entidades reaparecem em várias consultas com nomes físicos estáveis.

Evolução e manutenção

Adicionar capacidades sem apagar a superfície que aplicações já usam.

Linha 2.x para 3.x

EntityMap<T>, FluentMapper.Initialize(...) e a ponte de type map permanecem. Para o caminho histórico comum, a migração tende a ser atualização coordenada de pacotes, validação e testes.

Guia de migração ↗

Estado global delimitado

Dapper.Query<T>() normal usa o type map global do Dapper. Dommel também é global. O isolamento de FluentMapRuntime vale para as APIs mapeadas que recebem ou pertencem ao runtime.

Limites globais ↗

Matriz de providers

SQLite, SQL Server, PostgreSQL, MySQL e MariaDB possuem evidência real para Core e Dommel na matriz atual. Oracle e Firebird têm evidência Core, mas não certificação Dommel.

Matriz de compatibilidade ↗

Contratos verificáveis

A faixa atual documentada é Dapper [2.1.79,3.0.0) e Dommel [3.5.3,4.0.0). Canários experimentais não ampliam a matriz até atualização intencional e revisada.

Contrato legível por máquina ↗

Limitações e riscos

FluentMap complementa o Dapper; não é seu substituto nem um ORM completo.

A biblioteca não administra conexões, transações, migrations, tracking ou desenho de queries. Configuração global tardia pode conflitar com consultas concorrentes; inicialização deve ocorrer antes do uso e ser tratada como imutável.

Compatibilidade esperada por Dapper/ADO.NET não equivale a certificação do FluentMap. Certificação Core não implica Dommel. Também não há alegação de Native AOT completo: o caminho estrito gerado tem cobertura delimitada e formas incompatíveis podem usar fallback.

Os benchmarks usam BenchmarkDotNet e SQLite em cenários definidos no repositório. O baseline versionado traz "reportOnly": true; portanto, serve como evidência exploratória e detector de tendência, não como promessa geral de ganho de desempenho entre ambientes, providers ou workloads.

Aprendizados

Compatibilidade é uma propriedade sustentada por limites, documentação e testes.

O valor do map não está em esconder o banco, mas em localizar a tradução. Aliases, DTOs e FluentMap permanecem opções arquiteturais; a frequência de reuso, a diferença semântica e a necessidade de isolamento indicam qual delas custa menos.

Na evolução da biblioteca, recursos opt-in permitem ampliar materialização, diagnóstico e integração sem obrigar consumidores históricos a adotar tudo. Matrizes separadas e linguagem precisa impedem que uma evidência de teste seja generalizada além do que realmente cobre.