Estudio de caso arquitectónico · biblioteca de código abierto

Evolución del acceso a datos sin acoplar el dominio a la base de datos heredada

Una tabla CUSTOMER expone CUST_NO y LEGAL_NM; el dominio necesita expresarse como Customer.Id y Customer.Name. FluentMap hace explícita esa traducción sin agregar atributos de persistencia a la entidad.

El escenario empresarial es ilustrativo. Demuestra contratos públicos de la biblioteca; no representa a un cliente, una implementación ni una mejora de rendimiento en producción.

Contexto y motivación

El vocabulario de la base de datos no tiene que convertirse en el vocabulario del dominio.

Los esquemas heredados contienen abreviaturas, convenciones históricas y nombres compartidos por sistemas que no pueden renombrarse en una sola entrega. Repetir esos nombres en el modelo .NET transfiere el costo de persistencia a las reglas de negocio, las API y las pruebas.

El objetivo no es ocultar SQL ni crear un ORM completo. La consulta y la conexión siguen bajo la responsabilidad de Dapper, mientras una configuración tipada y centralizada traduce los nombres físicos a propiedades que comunican intención.

Restricciones y atributos de calidad

Modernizar el límite sin romper contratos históricos.

01

Esquema estable

Las tablas y columnas se comparten y es posible que no sigan inmediatamente el nombre del dominio.

02

Dominio limpio

Las entidades no reciben atributos que les permitan conocer el mecanismo de persistencia.

03

Compatibilidad

La línea 3.x conserva las principales API históricas de 2.x, con diferencias documentadas y nuevas funciones de suscripción.

04

Evidencia por capa

La compatibilidad con Dapper, la certificación Core y la integración con Dommel son afirmaciones distintas.

MantenibilidadCapacidad de pruebaCompatibilidadLegibilidadAislamientoCapacidad de evolución
Flujo de la tabla CUSTOMER con CUST_NO y LEGAL_NM, a través de Dapper y EntityMap, hasta Customer con Id y Name.
El resultado sigue proviniendo del SQL ejecutado por Dapper; el mapa define cómo las columnas materializan el objeto.

Decisiones arquitectónicas

El mapeo es un límite explícito, no una abstracción de todo el acceso a los datos.

Mapas tipados

EntityMap<T> asocia expresiones de propiedad con columnas. El cambio de nombre de una propiedad pasa por el compilador y la configuración puede validarse al iniciar la aplicación.

Mapeo de propiedades ↗

Convenciones con precedencia

Las convenciones reducen la repetición cuando el esquema sigue estándares. Los mapas explícitos siguen siendo adecuados para excepciones y tienen precedencia documentada.

Prioridad ↗

Configuración inmutable

La configuración global se construye antes de las consultas simultáneas y se trata como de solo lectura. FluentMapRuntime permite configuraciones aisladas en API QueryMapped*.

Aislamiento en tiempo de ejecución ↗

Dommel opcional

El paquete Dommel agrega metadatos y persistencia, pero mantiene el estado global en solucionadores y constructores. Los tiempos de ejecución aislados de FluentMap no aíslan a Dommel.

Integración Dommel ↗

Generación opt-in

Los source generators registran mapas elegibles y materializadores para formas compatibles. No analizan SQL ni garantizan todas las formas de materialización; los casos no compatibles recurren a un fallback.

Source generators ↗

Diagnóstico estático

Los analizadores detectan configuraciones observables en la compilación. El registro dinámico, la reflexión y el comportamiento dependiente del tiempo de ejecución quedan fuera de esta prueba.

Analizadores ↗

Implementación del escenario

Una entidad sin atributos, un mapa en la infraestructura y una 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");
    }
}

El ejemplo utiliza la API documentada actual. La conexión, transacción, parametrización, consulta y migraciones siguen siendo responsabilidad de la aplicación y de Dapper.

Flujo de materialización

El proveedor ADO.NET entrega a Dapper un lector con CUST_NO y LEGAL_NM. El mapa de tipos instalado por FluentMap resuelve estos nombres para Id y Name. Dapper crea y puebla Customer; FluentMap no rastrea la entidad ni cambia el SQL.

Alternativas y compensaciones

Tres enfoques válidos, con costos en diferentes lugares.

EnfoqueEjemploVentajaCosto y mejor contexto
Alias en SQLSELECT CUST_NO AS Id, LEGAL_NM AS NameSin configuración adicional; intención visible en la consulta.Repite alias en varias consultas. Es adecuado para una lectura puntual o para SQL controlado por una sola operación.
Persistencia DTOCustomerRow.CUST_NO → Customer.IdLímite y transformación totalmente explícitos; DTO puede reflejar uniones y proyecciones.Más tipos y código de conversión. Bueno cuando la forma de persistencia difiere semánticamente del agregado.
FluentMap explícitoMap(x => x.Id).ToColumn("CUST_NO")Reutiliza un mapa escrito y mantiene la entidad libre de atributos.La configuración global requiere disciplina; no elimina SQL. Es bueno cuando las entidades reaparecen en múltiples consultas con nombres físicos estables.

Evolución y mantenimiento

Agregue capacidades sin borrar la superficie que las aplicaciones ya usan.

Línea 2.x a 3.x

EntityMap<T>, FluentMapper.Initialize(...) y el puente del mapa tipo permanecen. Para la ruta histórica común, la migración tiende a consistir en actualizaciones, validación y pruebas de paquetes coordinadas.

Guía de migración ↗

Estado global acotado

El uso normal de Dapper.Query<T>() emplea el type map global de Dapper. Dommel también es global. El aislamiento de FluentMapRuntime se aplica a las API mapeadas que reciben el runtime o pertenecen a él.

Límites globales ↗

Matriz de proveedores

SQLite, SQL Server, PostgreSQL, MySQL y MariaDB tienen evidencia real de Core y Dommel en la matriz actual. Oracle y Firebird tienen evidencia Core, pero no certificación Dommel.

Matriz de compatibilidad ↗

Contratos verificables

El rango documentado actualmente es Dapper [2.1.79,3.0.0) y Dommel [3.5.3,4.0.0). Los canarios experimentales no amplían la matriz hasta que exista una actualización intencional y revisada.

Contrato legible por máquina ↗

Limitaciones y riesgos

FluentMap complementa a Dapper; no lo sustituye ni es un ORM completo.

La biblioteca no gestiona conexiones, transacciones, migraciones, seguimiento ni diseño de consultas. La configuración global tardía puede entrar en conflicto con consultas simultáneas; la inicialización debe ocurrir antes de su uso y ser tratada como inmutable.

La compatibilidad esperada por Dapper/ADO.NET no equivale a una certificación de FluentMap. La certificación de Core no implica la de Dommel. Tampoco se afirma compatibilidad completa con Native AOT: la ruta estricta generada tiene cobertura acotada y las formas incompatibles pueden usar fallback.

Los benchmarks usan BenchmarkDotNet y SQLite en escenarios definidos en el repositorio. La línea base versionada incluye "reportOnly": true; por lo tanto, sirve como evidencia exploratoria y detector de tendencias, no como promesa general de mejora de rendimiento entre entornos, providers o workloads.

Aprendizajes

La compatibilidad es una propiedad respaldada por límites, documentación y pruebas.

El valor del mapa no está en ocultar la base de datos, sino en encontrar la traducción. Los alias, DTO y FluentMap siguen siendo opciones arquitectónicas; la frecuencia de reutilización, la diferencia semántica y la necesidad de aislamiento indican cuál cuesta menos.

En la evolución de la biblioteca, los recursos opt-in permiten ampliar la materialización, el diagnóstico y la integración sin obligar a los consumidores históricos a adoptarlo todo. Las matrices separadas y el lenguaje preciso impiden que la evidencia de las pruebas se generalice más allá de lo que realmente cubre.