diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf7c831..c43a626 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -462,6 +462,7 @@ jobs: if: ${{ vars.SONAR_CI_ENABLED == 'true' && ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch') }} permissions: contents: read + id-token: write issues: write pull-requests: write @@ -538,6 +539,7 @@ jobs: pwsh -NoLogo -NoProfile -File './eng/run-sonar-tests.ps1' - name: Verify coverage report + id: verify_coverage shell: pwsh run: | $ErrorActionPreference = 'Stop' @@ -569,6 +571,15 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: dotnet sonarscanner end /d:sonar.token="${SONAR_TOKEN}" + - name: Upload coverage to Codecov + if: ${{ always() && steps.verify_coverage.outcome == 'success' && steps.sonar_end.outcome != 'cancelled' }} + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + files: ./artifacts/coverage/sonar/coverage.xml + disable_search: true + fail_ci_if_error: true + use_oidc: true + - name: Report Sonar quality gate conditions id: sonar_quality_gate if: ${{ always() && github.event_name == 'pull_request' }} diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 2bd6ded..4f290fd 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -120,7 +120,7 @@ The fork preserves the main historical source-compatible API surface where possi The fork also adds public APIs for profiles, naming policies, generated materializers, persistence metadata, property converters, query helpers, immutable configuration, isolated runtime and DI. -The fork-owned 3.0 line has published stable packages through 3.0.3. Public compatibility remains governed by SemVer and the package/API boundaries listed above. +The maintained 3.x line follows Semantic Versioning. See the GitHub releases page for the current stable version. Public compatibility remains governed by SemVer and the package/API boundaries listed above. ## Unsupported Environments Or Claims diff --git a/MIGRATION.md b/MIGRATION.md index b695e54..b4707f1 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,26 +1,32 @@ -# Migration Guide +# Migration Guide: 2.x to 3.x -This guide is for users moving from FluentMap 2.x to the 3.0 line in the original `Dapper.FluentMap` repository. +This guide is for users migrating from the historical Dapper.FluentMap 2.x line to the maintained 3.x line. ```text Dapper.FluentMap 2.x ↓ -Dapper.FluentMap 3.0 +Dapper.FluentMap 3.x ``` -Most existing root-level maps should not need source changes. The historical API remains supported while 3.0 adds opt-in capabilities for advanced materialization, generated registration, persistence metadata, property converters, profiles and isolated configuration. +For most applications that use root-level `EntityMap` mappings with `FluentMapper.Initialize(...)` and normal Dapper queries, the migration is primarily a package upgrade plus validation and testing. The historical API remains supported, and the newer 3.x capabilities are opt-in. -## What Stays Compatible +Do not rewrite working historical maps only because newer APIs exist. Adopt the newer APIs when they solve a concrete problem. -The following patterns remain the compatibility path: +## TL;DR + +If your application looks like this: ```csharp FluentMapper.Initialize(config => { config.AddMap(); }); + +var customer = connection.QuerySingle(sql); ``` +and your maps are root-level mappings such as: + ```csharp public sealed class CustomerMap : EntityMap { @@ -33,9 +39,77 @@ public sealed class CustomerMap : EntityMap } ``` -Normal Dapper calls such as `connection.Query()` continue to use Dapper's global type map bridge for root-level mappings installed by `FluentMapper.Initialize(...)`. +you normally do not need source changes to migrate to 3.x. + +Upgrade the package, validate the configuration, and run your application test suite. + +## Before You Upgrade + +Before changing FluentMap package versions: + +- ensure the application uses a supported Dapper version; +- if using Dommel, ensure it uses a supported Dommel version; +- keep all FluentMap packages on the same release version; +- run the existing test suite before and after the upgrade; +- identify whether the application uses Dommel, `Ignore()` for database-generated columns, assembly scanning, custom Dapper `TypeHandler` implementations, trimming or Native AOT. -Do not migrate working historical maps just because newer APIs exist. Prefer the newer APIs when they solve a concrete problem. +Current supported package ranges are documented in [COMPATIBILITY.md](COMPATIBILITY.md). At the time of this guide: + +```text +Dapper [2.1.79,3.0.0) +Dommel [3.5.3,4.0.0) +``` + +Provider certification and exact tested versions are also maintained in [COMPATIBILITY.md](COMPATIBILITY.md). + +## Minimal Migration + +For applications that only use the historical root-level mapping model, the recommended first migration step is intentionally small. + +Keep: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddMap(); + config.AddMap(); +}); + +FluentMapper.Validate(); +``` + +Keep normal Dapper calls: + +```csharp +var customer = connection.QuerySingle(sql); +var customers = connection.Query(sql); +``` + +Then: + +1. update FluentMap packages to the same 3.x release; +2. restore and build the application; +3. call `FluentMapper.Validate()` during startup or validation tests; +4. run unit and integration tests; +5. review the scenario-specific items below only when they apply. + +## Migration Decision Table + +| If your application uses | Migration action | +| --- | --- | +| `EntityMap` + normal `Dapper.Query()` | Usually no source changes. Keep the historical API. | +| `FluentMapper.Initialize(...)` | Keep it when the process has one effective global configuration. | +| Dommel | Review persistence and generated-column behavior and run real write tests. | +| `Ignore()` only to avoid writing a generated/default column | Replace that workaround with persistence metadata. | +| Nested objects | Use `QueryMapped*` only where FluentMap must materialize nested paths. | +| Value objects stored as one scalar value | Keep using Dapper `TypeHandler` when the representation is global for the type. | +| Value objects mapped through components | Use FluentMap-controlled materialization such as `QueryMapped*`. | +| Assembly scanning | It remains available; prefer explicit or generated registration for trimming/Native AOT. | +| Multiple mapping configurations in one process | Consider `FluentMapRuntime` and immutable configuration. | +| Dependency Injection | Add `FluentMap.DependencyInjection` only when host integration is needed. | +| Trimming or Native AOT | Prefer explicit/generated registration and review the documented AOT boundary. | +| Alternate SQL shapes for one entity | Consider mapping profiles. | +| Per-property database conversion | Consider property converters for FluentMap-controlled materialization. | ## Packages @@ -49,9 +123,51 @@ Install only the packages you use: | `FluentMap.Analyzers` | Compile-time diagnostics for mapping mistakes. | | `FluentMap.Generators` | Generated registration and supported generated materializers. | -The three `FluentMap.*` PackageIds replace the unpublished `Dapper.FluentMap.DependencyInjection`, `Dapper.FluentMap.Analyzers` and `Dapper.FluentMap.Generators` distribution identities. This is not a namespace, assembly or API rename; keep existing `using Dapper.FluentMap.*` directives. +The historical core PackageIds are unchanged: + +```text +Dapper.FluentMap -> Dapper.FluentMap +Dapper.FluentMap.Dommel -> Dapper.FluentMap.Dommel +``` + +The optional modern packages use these NuGet PackageIds: + +```text +FluentMap.DependencyInjection +FluentMap.Analyzers +FluentMap.Generators +``` + +These `FluentMap.*` names are distribution identities only. Assemblies, namespaces and public APIs remain under `Dapper.FluentMap.*`. -## Initialize +The three `FluentMap.*` PackageIds replace the unpublished `Dapper.FluentMap.DependencyInjection`, `Dapper.FluentMap.Analyzers` and `Dapper.FluentMap.Generators` distribution identities. + +## What Stays Compatible + +The following patterns remain the compatibility path: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddMap(); +}); +``` + +```csharp +public sealed class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Name).ToColumn("customer_name"); + Map(customer => customer.TransientValue).Ignore(); + } +} +``` + +Normal Dapper calls such as `connection.Query()` continue to use Dapper's global type map bridge for root-level mappings installed by `FluentMapper.Initialize(...)`. + +## Initialize And Validation The historical static initialization remains supported: @@ -67,7 +183,7 @@ FluentMapper.Validate(); Use this when your process has one effective mapping configuration and you want normal `Dapper.Query()` calls to use FluentMap's global Dapper type map bridge. -Version 3.0 also publishes `FluentMapper.Configuration` and `FluentMapper.Runtime` after initialization. Existing code does not need to use those properties. +Version 3.x also publishes `FluentMapper.Configuration` and `FluentMapper.Runtime` after initialization. Existing code does not need to use those properties. ## Registration @@ -94,7 +210,7 @@ Existing conventions remain supported: config.AddConvention().ForEntity(); ``` -Version 3.0 adds naming policies for common transformations: +Version 3.x adds naming policies for common transformations: ```csharp config.UseNamingPolicy(NamingPolicy.SnakeCase, caseSensitive: false) @@ -103,9 +219,59 @@ config.UseNamingPolicy(NamingPolicy.SnakeCase, caseSensitive: false) Precedence remains explicit mapping first, then convention/naming policy, then Dapper default behavior. +## Dommel And Ignore() + +If you use Dommel, review every `Ignore()` mapping before upgrading. + +`Ignore()` keeps its historical meaning: the property is not mapped for FluentMap materialization and is excluded from generated persistence metadata. + +If historical Dommel code used `Ignore()` only to avoid writing a database-generated column while still reading it, migrate that mapping to persistence metadata: + +```csharp +Map(entity => entity.CreatedAt) + .ToColumn("created_at") + .DatabaseDefaultOnInsert(); + +Map(entity => entity.UpdatedAt) + .ToColumn("updated_at") + .ReadOnly(); + +Map(entity => entity.Total) + .ToColumn("total") + .Computed(); +``` + +Use `Ignore()` only for values that should not be materialized by FluentMap. + +After upgrading a Dommel application, test at least: + +- inserts; +- updates; +- reads such as `Get`/`GetAll` used by the application; +- identity keys; +- database defaults; +- computed columns; +- read-only columns. + +## Persistence Semantics + +The core package stores persistence metadata. Dommel consumes this metadata for generated writes: + +| Mapping | Read | Insert | Update | +| --- | --- | --- | --- | +| default | yes | yes | yes | +| `Ignore()` | no | no | no | +| `ReadOnly()` | yes | no | no | +| `Computed()` | yes | no | no | +| `DatabaseDefaultOnInsert()` | yes | no | yes | +| `ExcludeFromInsert()` | yes | no | yes | +| `ExcludeFromUpdate()` | yes | yes | no | + +The core package still does not generate CRUD SQL. + ## Nested Objects -Historical FluentMap mainly helps Dapper map root-level members. Nested object materialization in 3.0 is opt-in: +Historical FluentMap mainly helps Dapper map root-level members. Nested object materialization in 3.x is opt-in: ```csharp public sealed class CustomerMap : EntityMap @@ -136,7 +302,7 @@ The current materializer uses compatible public constructors. Factory methods ar ## Profiles -Profiles are new opt-in mappings for alternate SQL shapes: +Profiles are opt-in mappings for alternate SQL shapes: ```csharp public sealed class LegacyProfile : IMappingProfile @@ -160,47 +326,9 @@ var customer = connection.QueryMappedSingle(sql); Profiles do not replace the default global Dapper type map. Select them per FluentMap-controlled query. -## Ignore - -`Ignore()` keeps its historical meaning: the property is not mapped for FluentMap materialization and is excluded from generated persistence metadata. - -If historical Dommel code used `Ignore()` only to avoid writing a database-generated column while still reading it, migrate that mapping to persistence metadata: - -```csharp -Map(entity => entity.CreatedAt) - .ToColumn("created_at") - .DatabaseDefaultOnInsert(); - -Map(entity => entity.UpdatedAt) - .ToColumn("updated_at") - .ReadOnly(); - -Map(entity => entity.Total) - .ToColumn("total") - .Computed(); -``` - -Use `Ignore()` only for values that should not be materialized by FluentMap. - -## Persistence Semantics - -The core package stores persistence metadata. Dommel consumes this metadata for generated writes: - -| Mapping | Read | Insert | Update | -| --- | --- | --- | --- | -| default | yes | yes | yes | -| `Ignore()` | no | no | no | -| `ReadOnly()` | yes | no | no | -| `Computed()` | yes | no | no | -| `DatabaseDefaultOnInsert()` | yes | no | yes | -| `ExcludeFromInsert()` | yes | no | yes | -| `ExcludeFromUpdate()` | yes | yes | no | - -The core package still does not generate CRUD SQL. - ## Property Converters -Property converters are new. They run only in FluentMap-controlled materialization: +Property converters run only in FluentMap-controlled materialization: ```csharp Map(product => product.Status) @@ -222,7 +350,7 @@ config.AddGeneratedMappings(); This can replace manual registration for eligible maps in the current compilation. It does not scan referenced assemblies and does not remove the need for runtime validation. -Generated materializers are an optimization. Unsupported cases fall back to runtime materialization. +Generated materializers are an optimization. Unsupported cases fall back to runtime materialization unless strict generated materialization is explicitly enabled. ## Configuration Isolation @@ -239,7 +367,7 @@ var customer = runtime.QueryMappedSingle(connection, sql); This isolates FluentMap-controlled materialization. It does not isolate normal `Dapper.Query()` because Dapper type maps are global per entity type. -## DI +## Dependency Injection Install `FluentMap.DependencyInjection` and register: @@ -252,7 +380,7 @@ services.AddFluentMap(builder => The DI package registers `ImmutableFluentMapConfiguration` and `FluentMapRuntime` as singletons. It does not register database connections, repositories, Dommel integration or global Dapper type maps. -## Dommel +## Dommel Configuration Dommel remains optional and process-wide: @@ -268,17 +396,39 @@ FluentMapper.Initialize(config => ## Breaking Or Risky Differences To Review -- Dommel persistence metadata has new behavior for read-only, computed, insert-excluded and update-excluded properties. +- Dommel persistence metadata has behavior for read-only, computed, insert-excluded and update-excluded properties; verify real write scenarios after upgrading. - Some contradictory configurations that were previously accepted by accident now fail validation. -- `DommelPropertyMap.GeneratedOption` has changed from non-nullable to nullable in the 3.0 line; treat binary compatibility with historical Dommel `2.0.0` as not guaranteed. -- Generated materialization and isolated runtime APIs are additive, but the stable release still requires a defined 3.0 API baseline. +- `DommelPropertyMap.GeneratedOption` changed from non-nullable to nullable in the 3.x line; treat binary compatibility with historical Dommel `2.0.0` as not guaranteed. +- Generated materialization and isolated runtime APIs are additive and do not require existing applications to change their historical mapping model. +- Normal `Dapper.Query()`, `FluentMapper.Initialize(...)` and Dommel still involve process-wide global state where documented. ## Recommended Migration Path -1. Keep existing `EntityMap` maps and `FluentMapper.Initialize(...)`. -2. Run the full test suite of your application against the 3.0 package. -3. Replace historical `Ignore()` write-workarounds with persistence metadata where needed. -4. Move nested/value-object reads to `QueryMapped*` only where required. -5. Add profiles only for alternate SQL shapes. -6. Add isolated runtime/DI only when you need multiple configurations or host integration. -7. Add analyzers and generators after the runtime behavior is already understood. +1. Confirm Dapper and, when applicable, Dommel versions are within the supported ranges. +2. Keep existing `EntityMap` maps and `FluentMapper.Initialize(...)`. +3. Upgrade all FluentMap packages to the same 3.x release. +4. Run `FluentMapper.Validate()` and the full application test suite. +5. If using Dommel, review every `Ignore()` workaround and test generated/default/computed/read-only columns. +6. Move nested/value-object reads to `QueryMapped*` only where required. +7. Add profiles only for alternate SQL shapes. +8. Add isolated runtime/DI only when you need multiple configurations or host integration. +9. Add analyzers and generators after the runtime behavior is already understood. +10. For trimming or Native AOT, review [COMPATIBILITY.md](COMPATIBILITY.md) and prefer explicit/generated registration. + +## Migration Checklist + +Your migration is ready when: + +- [ ] all FluentMap packages use the same 3.x release version; +- [ ] Dapper is inside the supported version range; +- [ ] Dommel is inside the supported version range, when used; +- [ ] the application builds successfully; +- [ ] `FluentMapper.Validate()` completes without validation errors; +- [ ] existing root-level queries behave as before; +- [ ] Dommel insert/update behavior has been tested, when applicable; +- [ ] identity, database-default, computed and read-only columns have been verified, when applicable; +- [ ] custom Dapper `TypeHandler` behavior has been verified, when used; +- [ ] integration tests pass against the application's actual database provider; +- [ ] trimming/Native AOT scenarios have been validated against the documented support boundary, when applicable. + +For current compatibility claims, provider certification and unsupported environments, see [COMPATIBILITY.md](COMPATIBILITY.md). diff --git a/MIGRATION.pt-BR.md b/MIGRATION.pt-BR.md new file mode 100644 index 0000000..616f93e --- /dev/null +++ b/MIGRATION.pt-BR.md @@ -0,0 +1,434 @@ +# Guia de Migração: 2.x para 3.x + +Este guia é destinado a usuários que estão migrando da linha histórica Dapper.FluentMap 2.x para a linha 3.x mantida atualmente. + +```text +Dapper.FluentMap 2.x + ↓ +Dapper.FluentMap 3.x +``` + +Para a maioria das aplicações que usam mappings raiz com `EntityMap`, `FluentMapper.Initialize(...)` e queries normais do Dapper, a migração é principalmente uma atualização de pacote seguida de validação e testes. A API histórica continua suportada e os recursos mais novos da linha 3.x são opt-in. + +Não reescreva mappings históricos que já funcionam apenas porque APIs mais novas existem. Adote essas APIs quando elas resolverem um problema concreto. + +## Resumo Rápido + +Se a aplicação usa algo como: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddMap(); +}); + +var customer = connection.QuerySingle(sql); +``` + +com mappings raiz como: + +```csharp +public sealed class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Name).ToColumn("customer_name"); + Map(customer => customer.TransientValue).Ignore(); + } +} +``` + +normalmente não são necessárias mudanças de código-fonte para migrar para a 3.x. + +Atualize o pacote, valide a configuração e execute a suíte de testes da aplicação. + +## Antes de Atualizar + +Antes de alterar as versões dos pacotes FluentMap: + +- confirme que a aplicação usa uma versão suportada do Dapper; +- se usar Dommel, confirme que está em uma versão suportada; +- mantenha todos os pacotes FluentMap na mesma versão de release; +- execute a suíte de testes existente antes e depois da atualização; +- identifique se a aplicação usa Dommel, `Ignore()` para colunas geradas pelo banco, assembly scanning, implementações customizadas de `TypeHandler` do Dapper, trimming ou Native AOT. + +As faixas suportadas atualmente ficam documentadas em [COMPATIBILITY.md](COMPATIBILITY.md). No momento deste guia: + +```text +Dapper [2.1.79,3.0.0) +Dommel [3.5.3,4.0.0) +``` + +A certificação de providers e as versões exatas testadas também são mantidas em [COMPATIBILITY.md](COMPATIBILITY.md). + +## Migração Mínima + +Para aplicações que usam apenas o modelo histórico de mapping no nível raiz, o primeiro passo recomendado é intencionalmente pequeno. + +Mantenha: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddMap(); + config.AddMap(); +}); + +FluentMapper.Validate(); +``` + +Mantenha as chamadas normais do Dapper: + +```csharp +var customer = connection.QuerySingle(sql); +var customers = connection.Query(sql); +``` + +Depois: + +1. atualize todos os pacotes FluentMap para a mesma release 3.x; +2. restaure e compile a aplicação; +3. execute `FluentMapper.Validate()` no startup ou em testes de validação; +4. execute os testes unitários e de integração; +5. revise os itens específicos de cenário abaixo somente quando se aplicarem. + +## Tabela de Decisão da Migração + +| Se a aplicação usa | Ação de migração | +| --- | --- | +| `EntityMap` + `Dapper.Query()` normal | Normalmente nenhuma mudança de código. Mantenha a API histórica. | +| `FluentMapper.Initialize(...)` | Mantenha quando o processo possui uma única configuração global efetiva. | +| Dommel | Revise o comportamento de persistência e colunas geradas e execute testes reais de escrita. | +| `Ignore()` apenas para evitar escrever uma coluna gerada/default | Substitua esse workaround por metadata de persistência. | +| Objetos aninhados | Use `QueryMapped*` somente onde FluentMap precisar materializar caminhos aninhados. | +| Value objects armazenados como um único valor escalar | Continue usando `TypeHandler` do Dapper quando a representação for global para o tipo. | +| Value objects mapeados por componentes | Use materialização controlada pelo FluentMap, como `QueryMapped*`. | +| Assembly scanning | Continua disponível; prefira registro explícito ou gerado para trimming/Native AOT. | +| Múltiplas configurações de mapping no mesmo processo | Considere `FluentMapRuntime` e configuração imutável. | +| Dependency Injection | Adicione `FluentMap.DependencyInjection` somente quando precisar de integração com o host. | +| Trimming ou Native AOT | Prefira registro explícito/gerado e revise os limites documentados de AOT. | +| Formatos SQL alternativos para a mesma entidade | Considere mapping profiles. | +| Conversão de banco específica por propriedade | Considere property converters na materialização controlada pelo FluentMap. | + +## Pacotes + +Instale somente os pacotes que utiliza: + +| Pacote | Quando instalar | +| --- | --- | +| `Dapper.FluentMap` | Mapping principal e integração com Dapper. | +| `Dapper.FluentMap.Dommel` | Integração Dommel para tabela, chave e colunas geradas. | +| `FluentMap.DependencyInjection` | Registro em DI da configuração imutável e runtime. | +| `FluentMap.Analyzers` | Diagnósticos em tempo de compilação para erros de mapping. | +| `FluentMap.Generators` | Registro gerado e materializadores gerados suportados. | + +Os PackageIds históricos dos pacotes principais não mudaram: + +```text +Dapper.FluentMap -> Dapper.FluentMap +Dapper.FluentMap.Dommel -> Dapper.FluentMap.Dommel +``` + +Os pacotes modernos opcionais usam estes NuGet PackageIds: + +```text +FluentMap.DependencyInjection +FluentMap.Analyzers +FluentMap.Generators +``` + +Os nomes `FluentMap.*` são apenas identidades de distribuição. Assemblies, namespaces e APIs públicas continuam sob `Dapper.FluentMap.*`. + +Os três PackageIds `FluentMap.*` substituem as identidades de distribuição não publicadas `Dapper.FluentMap.DependencyInjection`, `Dapper.FluentMap.Analyzers` e `Dapper.FluentMap.Generators`. + +## O Que Continua Compatível + +Os padrões abaixo continuam sendo o caminho de compatibilidade: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddMap(); +}); +``` + +```csharp +public sealed class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Name).ToColumn("customer_name"); + Map(customer => customer.TransientValue).Ignore(); + } +} +``` + +Chamadas normais do Dapper como `connection.Query()` continuam usando o bridge global de type map do Dapper para mappings raiz instalados por `FluentMapper.Initialize(...)`. + +## Inicialização e Validação + +A inicialização estática histórica continua suportada: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddMap(); + config.AddMap(); +}); + +FluentMapper.Validate(); +``` + +Use esse modelo quando o processo possui uma única configuração efetiva e você quer que chamadas normais de `Dapper.Query()` usem o bridge global de type map do FluentMap. + +A linha 3.x também publica `FluentMapper.Configuration` e `FluentMapper.Runtime` após a inicialização. Código existente não precisa usar essas propriedades. + +## Registro + +Registros explícitos existentes continuam válidos: + +```csharp +config.AddMap(); +config.AddMap(new CustomerMap()); +``` + +Assembly scanning também continua disponível: + +```csharp +config.AddMapsFromAssemblyContaining(); +``` + +Para deployments com trimming e Native AOT, prefira registro explícito ou gerado em vez de assembly scanning. + +## Convenções + +Convenções existentes continuam suportadas: + +```csharp +config.AddConvention().ForEntity(); +``` + +A linha 3.x adiciona naming policies para transformações comuns: + +```csharp +config.UseNamingPolicy(NamingPolicy.SnakeCase, caseSensitive: false) + .ForEntity(); +``` + +A precedência continua sendo mapping explícito primeiro, depois convenção/naming policy e por fim o comportamento padrão do Dapper. + +## Dommel e Ignore() + +Se você usa Dommel, revise todo mapping com `Ignore()` antes de atualizar. + +`Ignore()` mantém seu significado histórico: a propriedade não é mapeada para materialização do FluentMap e fica fora da metadata de persistência gerada. + +Se código Dommel histórico usava `Ignore()` apenas para evitar escrever uma coluna gerada pelo banco, mas ainda precisava lê-la, migre esse mapping para metadata de persistência: + +```csharp +Map(entity => entity.CreatedAt) + .ToColumn("created_at") + .DatabaseDefaultOnInsert(); + +Map(entity => entity.UpdatedAt) + .ToColumn("updated_at") + .ReadOnly(); + +Map(entity => entity.Total) + .ToColumn("total") + .Computed(); +``` + +Use `Ignore()` somente para valores que não devem ser materializados pelo FluentMap. + +Após atualizar uma aplicação Dommel, teste pelo menos: + +- inserts; +- updates; +- leituras como `Get`/`GetAll` usadas pela aplicação; +- chaves identity; +- defaults do banco; +- colunas computadas; +- colunas somente leitura. + +## Semântica de Persistência + +O pacote core armazena metadata de persistência. Dommel consome essa metadata para escritas geradas: + +| Mapping | Leitura | Insert | Update | +| --- | --- | --- | --- | +| padrão | sim | sim | sim | +| `Ignore()` | não | não | não | +| `ReadOnly()` | sim | não | não | +| `Computed()` | sim | não | não | +| `DatabaseDefaultOnInsert()` | sim | não | sim | +| `ExcludeFromInsert()` | sim | não | sim | +| `ExcludeFromUpdate()` | sim | sim | não | + +O pacote core continua sem gerar SQL CRUD. + +## Objetos Aninhados + +Historicamente, FluentMap ajuda principalmente o Dapper a mapear membros no nível raiz. A materialização de objetos aninhados na linha 3.x é opt-in: + +```csharp +public sealed class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(customer => customer.Address.City).ToColumn("city"); + } +} + +var customer = connection.QueryMappedSingle( + "SELECT 'Sao Paulo' AS city;"); +``` + +Use `QueryMapped*`, `ReadMapped*`, `QueryMultipleMapped` ou helpers de streaming quando FluentMap precisar materializar caminhos aninhados. `Dapper.Query()` normal não se transforma em um graph mapper. + +## Value Objects + +Para um value object armazenado como um único valor do banco, continue usando `TypeHandler` do Dapper quando essa representação for global para o tipo. + +Para value objects armazenados por componentes mapeados, use materialização controlada pelo FluentMap: + +```csharp +Map(customer => customer.Cpf.Number).ToColumn("cpf"); +``` + +O materializador atual usa construtores públicos compatíveis. Factory methods não são utilizados. + +## Profiles + +Profiles são mappings opt-in para formatos SQL alternativos: + +```csharp +public sealed class LegacyProfile : IMappingProfile +{ +} + +public sealed class LegacyCustomerMap : + EntityMap, + IProfileMap +{ + public LegacyCustomerMap() + { + Map(customer => customer.Name).ToColumn("legacy_name"); + } +} + +config.AddProfile(); + +var customer = connection.QueryMappedSingle(sql); +``` + +Profiles não substituem o type map global padrão do Dapper. Selecione-os por query controlada pelo FluentMap. + +## Conversores de Propriedade + +Property converters são executados somente na materialização controlada pelo FluentMap: + +```csharp +Map(product => product.Status) + .ToColumn("status_code") + .ConvertFromDatabaseUsing(); +``` + +`Dapper.Query()` normal não executa property converters. Use `TypeHandler` do Dapper para conversões globais por tipo. + +Existe metadata de write converter, mas escritas Dapper/Dommel ainda não a executam. + +## Registro Gerado + +Instale `FluentMap.Generators` e chame: + +```csharp +config.AddGeneratedMappings(); +``` + +Isso pode substituir registro manual para maps elegíveis da compilação atual. Não faz scan de assemblies referenciados e não elimina a necessidade de validação em runtime. + +Materializadores gerados são uma otimização. Casos não suportados usam materialização runtime como fallback, exceto quando o modo estrito de materialização gerada é habilitado explicitamente. + +## Isolamento de Configuração + +Se a aplicação precisa de múltiplas configurações FluentMap no mesmo processo, use configuração imutável e instâncias de runtime: + +```csharp +var runtime = new FluentMapConfigurationBuilder() + .AddMap() + .Build() + .CreateRuntime(); + +var customer = runtime.QueryMappedSingle(connection, sql); +``` + +Isso isola a materialização controlada pelo FluentMap. Não isola `Dapper.Query()` normal, porque os type maps do Dapper são globais por tipo de entidade. + +## Dependency Injection + +Instale `FluentMap.DependencyInjection` e registre: + +```csharp +services.AddFluentMap(builder => +{ + builder.AddMap(); +}); +``` + +O pacote de DI registra `ImmutableFluentMapConfiguration` e `FluentMapRuntime` como singletons. Ele não registra conexões de banco, repositories, integração Dommel ou type maps globais do Dapper. + +## Configuração do Dommel + +Dommel continua opcional e process-wide: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddMap(); + config.ForDommel(); +}); +``` + +`DommelEntityMap`, `IsKey()`, `IsIdentity()` e `SetGeneratedOption(...)` continuam sendo a superfície de mapping específica do Dommel. Runtimes FluentMap isolados não configuram Dommel. + +## Diferenças Quebráveis ou de Risco a Revisar + +- A metadata de persistência Dommel possui comportamento para propriedades read-only, computed, excluídas de insert e excluídas de update; valide cenários reais de escrita após a atualização. +- Algumas configurações contraditórias que antes eram aceitas acidentalmente agora falham na validação. +- `DommelPropertyMap.GeneratedOption` mudou de não anulável para anulável na linha 3.x; considere que compatibilidade binária com o Dommel histórico `2.0.0` não é garantida. +- Materialização gerada e APIs de runtime isolado são aditivas e não exigem que aplicações existentes alterem seu modelo histórico de mapping. +- `Dapper.Query()` normal, `FluentMapper.Initialize(...)` e Dommel ainda envolvem estado global process-wide onde documentado. + +## Caminho de Migração Recomendado + +1. Confirme que as versões do Dapper e, quando aplicável, Dommel estão dentro das faixas suportadas. +2. Mantenha os mappings `EntityMap` existentes e `FluentMapper.Initialize(...)`. +3. Atualize todos os pacotes FluentMap para a mesma release 3.x. +4. Execute `FluentMapper.Validate()` e a suíte completa de testes da aplicação. +5. Se usar Dommel, revise cada workaround com `Ignore()` e teste colunas geradas/default/computed/read-only. +6. Mova leituras de objetos aninhados/value objects para `QueryMapped*` somente onde necessário. +7. Adicione profiles apenas para formatos SQL alternativos. +8. Adicione runtime isolado/DI apenas quando precisar de múltiplas configurações ou integração com host. +9. Adicione analyzers e generators depois que o comportamento de runtime já estiver compreendido. +10. Para trimming ou Native AOT, revise [COMPATIBILITY.md](COMPATIBILITY.md) e prefira registro explícito/gerado. + +## Checklist da Migração + +A migração está pronta quando: + +- [ ] todos os pacotes FluentMap usam a mesma versão de release 3.x; +- [ ] o Dapper está dentro da faixa de versões suportada; +- [ ] o Dommel está dentro da faixa suportada, quando utilizado; +- [ ] a aplicação compila com sucesso; +- [ ] `FluentMapper.Validate()` termina sem erros de validação; +- [ ] as queries raiz existentes se comportam como antes; +- [ ] o comportamento de insert/update do Dommel foi testado, quando aplicável; +- [ ] colunas identity, database-default, computed e read-only foram verificadas, quando aplicável; +- [ ] o comportamento de `TypeHandler` customizado do Dapper foi verificado, quando utilizado; +- [ ] os testes de integração passam contra o provider de banco realmente usado pela aplicação; +- [ ] cenários com trimming/Native AOT foram validados contra os limites de suporte documentados, quando aplicável. + +Para as afirmações atuais de compatibilidade, certificação de providers e ambientes não suportados, consulte [COMPATIBILITY.md](COMPATIBILITY.md). diff --git a/README.md b/README.md index 13f944a..b2581de 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,56 @@ # FluentMap [![CI](https://github.com/rodri-oliveira-dev/Dapper-FluentMap/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/rodri-oliveira-dev/Dapper-FluentMap/actions/workflows/ci.yml) +[![CodeQL](https://github.com/rodri-oliveira-dev/Dapper-FluentMap/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/rodri-oliveira-dev/Dapper-FluentMap/actions/workflows/codeql.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=rodri-oliveira-dev_Dapper-FluentMap&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=rodri-oliveira-dev_Dapper-FluentMap) -[![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4?logo=dotnet&logoColor=white)](https://learn.microsoft.com/dotnet/standard/net-standard) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=rodri-oliveira-dev_Dapper-FluentMap&metric=coverage)](https://sonarcloud.io/summary/new_code?id=rodri-oliveira-dev_Dapper-FluentMap) +[![codecov](https://codecov.io/github/rodri-oliveira-dev/Dapper-FluentMap/branch/main/graph/badge.svg)](https://codecov.io/github/rodri-oliveira-dev/Dapper-FluentMap) +[![NuGet](https://img.shields.io/nuget/v/Dapper.FluentMap?logo=nuget)](https://www.nuget.org/packages/Dapper.FluentMap) +[![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4?logo=dotnet&logoColor=white)](https://learn.microsoft.com/dotnet/standard/net-standard) [![License: MIT](https://img.shields.io/github/license/rodri-oliveira-dev/Dapper-FluentMap)](LICENSE) [![GitHub stars](https://img.shields.io/github/stars/rodri-oliveira-dev/Dapper-FluentMap?style=flat&logo=github)](https://github.com/rodri-oliveira-dev/Dapper-FluentMap/stargazers) English | [Português (Brasil)](README.pt-BR.md) -FluentMap is an advanced mapping layer for Dapper. It lets you describe how .NET object properties map to database columns with fluent, strongly typed code, while keeping persistence attributes out of your POCOs. +FluentMap is an advanced mapping layer for Dapper. It lets you describe how .NET object properties map to database columns with fluent, strongly typed code while keeping persistence attributes out of your POCOs. -FluentMap is not an ORM. It does not track entities, build arbitrary SQL, manage connections, run migrations, provide LINQ, or replace Dapper. Use it when Dapper's default name-based mapping is not enough and the mapping rules should live outside the model. +FluentMap is not an ORM. It does not track entities, build arbitrary SQL, manage connections, run migrations, provide LINQ, or replace Dapper. ## Project Status -Dapper.FluentMap is being actively modernized again. Version 3.0 continues the original project history while preserving the core FluentMap mapping model and compatibility path. - -The 3.0 line modernizes the library and adds new opt-in capabilities without turning FluentMap into an ORM or requiring existing applications to adopt the new APIs. - -## Coming from FluentMap 2.x? - -If you used FluentMap before and are returning to the project, the important compatibility points are: - -- existing `EntityMap` mappings remain supported; -- `FluentMapper.Initialize(...)` remains supported; -- normal `Dapper.Query()` calls continue to work with root-level mappings installed through the historical static API; -- most existing root-level maps should not need source changes; -- most 3.0 capabilities are opt-in, so you do not need to rewrite working mappings just because newer APIs exist. - -See [MIGRATION.md](MIGRATION.md) for the recommended 2.x to 3.0 migration path and the behavioral differences worth reviewing. +Dapper.FluentMap is actively maintained. The maintained 3.x line continues the original project history while preserving the historical mapping model and adding newer capabilities as opt-in features. -## What's New in 3.0 +Existing `EntityMap` mappings and `FluentMapper.Initialize(...)` remain the compatibility baseline. Applications using normal root-level Dapper mappings generally do not need to rewrite working maps when upgrading from 2.x. -FluentMap 3.0 modernizes the original project without changing its core purpose. In addition to the historical fluent mapping API, the 3.0 line adds opt-in support for: +See [MIGRATION.md](MIGRATION.md) when moving from FluentMap 2.x. -- immutable constructor mapping improvements; -- nested object and value object materialization; -- mapping profiles for alternate SQL shapes; -- mapped `QueryMultiple`, unbuffered reads and async streaming; -- property conversion metadata and diagnostics; -- source-generated map registration and supported materializers; -- Roslyn analyzers for mapping diagnostics; -- isolated immutable configuration and dependency injection; -- richer persistence metadata consumed by the Dommel integration; -- trimming/AOT-aware registration and diagnostics; -- modern compatibility tests, provider harnesses, benchmarks, CI and package validation. +## Key Capabilities -Existing mappings remain the compatibility baseline. Adopt the newer APIs only when they solve a concrete problem. - -## Positioning - -Use FluentMap for: - -- explicit property-to-column maps; -- conventions and naming policies; -- ignored properties; -- immutable constructor mapping; -- opt-in nested object and value object materialization; -- mapping profiles for alternate SQL shapes; -- generated map registration/materialization where supported; -- persistence metadata consumed by integrations such as Dommel; -- isolated configuration and dependency injection for FluentMap-controlled materialization. - -Do not use FluentMap as an ORM, CRUD framework, query builder, unit of work, or database abstraction. +| Capability | Main API | +| --- | --- | +| Explicit property-to-column mapping | `EntityMap`, `Map(...).ToColumn(...)` | +| Conventions and naming policies | `AddConvention(...)`, `UseNamingPolicy(...)` | +| Immutable constructor mapping | historical Dapper type-map bridge | +| Nested objects and component value objects | `QueryMapped*` | +| Alternate SQL shapes | mapping profiles | +| Two-type multi-mapping | `QueryMapped(...)` | +| Multiple result sets | `QueryMultipleMapped*`, `ReadMapped*` | +| Sync/async streaming | `QueryMappedUnbuffered*` | +| Per-property conversion | property converters | +| Generated registration/materialization | `AddGeneratedMappings()` | +| Strict generated path | `UseStrictGeneratedMaterialization()`, `QueryGeneratedMapped*` | +| Isolated configuration | `FluentMapRuntime` | +| Dependency Injection | `AddFluentMap(...)` | +| Dommel persistence metadata | `Dapper.FluentMap.Dommel` | +| Compile-time diagnostics | `FluentMap.Analyzers` | + +Detailed examples are in [USAGE.md](USAGE.md). ## Installation -Install the package that matches the feature set you need: +Install only the packages required by your application: -| Package purpose | NuGet PackageId | +| Purpose | NuGet PackageId | | --- | --- | | Core | `Dapper.FluentMap` | | Dommel integration | `Dapper.FluentMap.Dommel` | @@ -77,17 +58,15 @@ Install the package that matches the feature set you need: | Roslyn analyzers | `FluentMap.Analyzers` | | Source generators | `FluentMap.Generators` | +Core package: + ```bash dotnet add package Dapper.FluentMap -dotnet add package Dapper.FluentMap.Dommel -dotnet add package FluentMap.DependencyInjection -dotnet add package FluentMap.Analyzers -dotnet add package FluentMap.Generators ``` -The `FluentMap.*` PackageIds are distribution identities only. They do not rename the existing assemblies, C# namespaces or public APIs. +The `FluentMap.*` PackageIds are distribution identities only. Assemblies, namespaces and public APIs remain under `Dapper.FluentMap.*`. -The public packages target `netstandard2.0`. See [COMPATIBILITY.md](COMPATIBILITY.md) before adopting a new release. +Public packages target `netstandard2.0`. Supported dependency ranges and certified providers are documented in [COMPATIBILITY.md](COMPATIBILITY.md). ## Quick Start @@ -107,6 +86,7 @@ public sealed class CustomerMap : EntityMap public CustomerMap() { Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Name).ToColumn("customer_name"); } } @@ -116,406 +96,75 @@ FluentMapper.Initialize(config => }); var customer = connection.QuerySingle( - "SELECT 7 AS customer_id, 'Ada' AS Name;"); + "SELECT 7 AS customer_id, 'Ada' AS customer_name;"); ``` Call `FluentMapper.Initialize(...)` during application startup and treat the effective global configuration as read-only once queries begin. -## Mapping - -Create maps by deriving from `EntityMap`: - -```csharp -public sealed class ProductMap : EntityMap -{ - public ProductMap() - { - Map(product => product.Id).ToColumn("product_id"); - Map(product => product.Name).ToColumn("product_name", caseSensitive: false); - Map(product => product.TransientValue).Ignore(); - } -} -``` - -Explicit mappings take precedence over conventions. Unmapped root members fall back to Dapper's normal behavior. - -Conventions and naming policies cover repeated patterns: - -```csharp -using Dapper.FluentMap.Conventions; -using Dapper.FluentMap.Naming; - -public sealed class PrefixConvention : Convention -{ - public PrefixConvention() - { - Properties().Configure(property => property.HasPrefix("col")); - } -} - -FluentMapper.Initialize(config => -{ - config.AddConvention().ForEntity(); - config.UseNamingPolicy(NamingPolicy.SnakeCase, caseSensitive: false) - .ForEntity(); -}); -``` - -Available naming policies include `Identity`, `SnakeCase`, `Prefix(...)`, `Suffix(...)`, `Custom(...)`, `Then(...)`, `WithPrefix(...)` and `WithSuffix(...)`. - -## Immutable Types - -FluentMap participates in Dapper constructor mapping for root-level explicit mappings: - -```csharp -public sealed class Customer -{ - public Customer(int id, string fullName) - { - Id = id; - FullName = fullName; - } - - public int Id { get; } - public string FullName { get; } -} - -public sealed class CustomerMap : EntityMap -{ - public CustomerMap() - { - Map(customer => customer.Id).ToColumn("customer_id"); - Map(customer => customer.FullName).ToColumn("full_name"); - } -} -``` - -Use `QueryMapped*` when FluentMap must construct nested immutable objects or value objects. - -## Nested Objects - -Nested member paths use the same `Map(...)` API: - -```csharp -public sealed class CustomerMap : EntityMap -{ - public CustomerMap() - { - Map(customer => customer.Id).ToColumn("customer_id"); - Map(customer => customer.Address.City).ToColumn("city"); - } -} - -var customer = connection.QueryMappedSingle( - "SELECT 7 AS customer_id, 'Sao Paulo' AS city;"); -``` - -Nested object materialization is opt-in through `QueryMapped*`, `ReadMapped*`, `QueryMultipleMapped` and streaming helpers. Normal `Dapper.Query()` remains root-level Dapper materialization. - -## Value Objects - -For scalar value objects mapped as one database value, prefer a Dapper `TypeHandler`: +For configuration validation: ```csharp -Map(customer => customer.Cpf).ToColumn("cpf"); -``` - -For value objects mapped through components, FluentMap-controlled materialization can call matching public constructors: - -```csharp -public sealed class CustomerMap : EntityMap -{ - public CustomerMap() - { - Map(customer => customer.Id).ToColumn("customer_id"); - Map(customer => customer.Cpf.Number).ToColumn("cpf"); - } -} - -var customer = connection.QueryMappedSingle( - "SELECT 1 AS customer_id, '12345678909' AS cpf;"); +FluentMapper.Validate(); ``` -Factory methods are not used by the current materializer. - -## Profiles +## Advanced Usage -Profiles are opt-in mappings for the same entity under different SQL shapes: - -```csharp -using Dapper.FluentMap.Mapping; - -public sealed class LegacyProfile : IMappingProfile -{ -} +Use FluentMap-controlled query APIs when mapping requires behavior beyond the historical root-level Dapper type map. -public sealed class LegacyCustomerMap : - EntityMap, - IProfileMap -{ - public LegacyCustomerMap() - { - Map(customer => customer.Id).ToColumn("id"); - Map(customer => customer.Name).ToColumn("legal_name"); - } -} +Examples include: -FluentMapper.Initialize(config => -{ - config.AddMap(); - config.AddProfile(); -}); +- nested objects and component value objects; +- profiles; +- two-type `splitOn` multi-mapping; +- mapped multiple result sets; +- sync/async streaming; +- property converters; +- generated and strict generated materialization; +- isolated runtimes and DI; +- Dommel persistence metadata. -var legacy = connection.QueryMappedSingle( - "SELECT 7 AS id, 'Legacy Ltd.' AS legal_name;"); -``` +See [USAGE.md](USAGE.md) for complete examples and API guidance. -Profiles are selected per FluentMap-controlled query. They do not replace the global Dapper type map for the entity. +## Migrating From 2.x -## Generated Materialization +The 3.x line preserves the main historical source-compatible mapping path. -Install `FluentMap.Generators` when you want generated registration for maps in the current compilation: +If your application uses `EntityMap`, `FluentMapper.Initialize(...)` and normal `Dapper.Query()` calls for root-level mappings, migration is usually a package upgrade followed by configuration validation and application testing. -```bash -dotnet add package FluentMap.Generators -``` +See [MIGRATION.md](MIGRATION.md) for: -Then call the generated extension: - -```csharp -FluentMapper.Initialize(config => -{ - config.AddGeneratedMappings(); -}); -``` - -The generator emits `AddMap()` and `AddProfile()` calls for eligible maps. For supported explicit mappings it can also register generated row materializers for the ordered column shape, including flat properties, nested paths, constructor-built value objects and statically supported read converters. - -Generated materialization is an optimization. Unsupported maps, dynamic shapes, shape mismatches, instance/delegate converters and some advanced patterns use the runtime fallback. - -## Persistence Semantics - -Persistence metadata describes write participation without changing read materialization: - -```csharp -Map(product => product.CreatedAt) - .ToColumn("created_at") - .DatabaseDefaultOnInsert(); - -Map(product => product.UpdatedAt) - .ToColumn("updated_at") - .ReadOnly(); - -Map(product => product.Total) - .ToColumn("total") - .Computed(); -``` - -`Ignore()` keeps its historical meaning: the property is not materialized by FluentMap and is not part of generated persistence metadata. For database values that should still be selected but not written, use `ReadOnly()`, `Computed()`, `DatabaseDefaultOnInsert()`, `ExcludeFromInsert()` or `ExcludeFromUpdate()`. - -The core package stores metadata. Dommel is the current package that consumes it for generated `INSERT` and `UPDATE` behavior. - -## QueryMultiple / Streaming - -Use FluentMap query helpers when materialization must honor nested mappings, value objects, profiles, converters or generated materializers: - -```csharp -var customers = connection.QueryMapped(sql); -var customer = connection.QueryMappedSingle(sql); -var legacy = connection.QueryMappedSingle(legacySql); -``` - -For two-entity JOIN rows, use explicit `splitOn` and a composition delegate: - -```csharp -var rows = connection.QueryMapped( - sql, - (customer, order) => new CustomerOrder(customer, order), - splitOn: "order_id"); -``` - -Each segment is materialized with its own FluentMap mapping. When profiles differ per segment, use the profile overload, for example `QueryMapped(...)`. If every column in the second segment is `NULL`, the second argument is `null`, which matches common `LEFT JOIN` child-absence semantics. - -For multiple result sets: - -```csharp -using var multi = connection.QueryMultipleMapped(sql); - -var customers = multi.ReadMapped(); -var orders = multi.ReadMapped(); -``` - -`ReadMapped*` consumes result sets sequentially and buffers the current result set. Concurrent reads on the same `MappedGridReader` are not supported; a competing read fails deterministically with `InvalidOperationException`. Async callers can use the corresponding async APIs: - -```csharp -await using var multi = await connection.QueryMultipleMappedAsync( - sql, - cancellationToken: cancellationToken); - -var customers = await multi.ReadMappedAsync(cancellationToken); -var orders = await multi.ReadMappedAsync(cancellationToken); -``` - -For incremental processing: - -```csharp -foreach (var customer in connection.QueryMappedUnbuffered(sql)) -{ - Process(customer); -} -``` - -Async streaming is available on `DbConnection`: - -```csharp -await foreach (var customer in connection.QueryMappedUnbufferedAsync( - sql, - cancellationToken)) -{ - await ProcessAsync(customer, cancellationToken); -} -``` - -Streaming keeps the underlying reader open until enumeration completes or the enumerator is disposed. - -## Property Converters - -Property converters are configured per mapped property and run only during FluentMap-controlled materialization: - -```csharp -public sealed class ProductMap : EntityMap -{ - public ProductMap() - { - Map(product => product.Status) - .ToColumn("status_code") - .ConvertFromDatabaseUsing(); - } -} - -public sealed class ProductStatusConverter : - IReadPropertyConverter -{ - public ProductStatus ConvertFromDatabase(string value) - { - return value == "A" ? ProductStatus.Active : ProductStatus.Inactive; - } -} -``` - -Read conversion precedence in FluentMap-controlled materialization is: - -```text -null/DBNull handling - -> property read converter - -> Dapper TypeHandler - -> FluentMap default conversion -``` - -Write converter metadata can be configured, but it is not currently executed by Dapper or Dommel writes. - -## Isolated Configuration / DI - -The historical static API remains supported: - -```csharp -FluentMapper.Initialize(config => -{ - config.AddMap(); -}); -``` - -For multiple FluentMap-controlled configurations in the same process, build immutable configurations and use their runtimes: - -```csharp -using Dapper.FluentMap.Configuration; - -var runtime = new FluentMapConfigurationBuilder() - .AddMap() - .Build() - .CreateRuntime(); - -var customer = runtime.QueryMappedSingle( - connection, - "SELECT 7 AS customer_id, 'Ada' AS Name;"); -``` - -Install `FluentMap.DependencyInjection` for DI registration: - -```bash -dotnet add package FluentMap.DependencyInjection -``` - -```csharp -using Microsoft.Extensions.DependencyInjection; - -services.AddFluentMap(builder => -{ - builder.AddMap(); - builder.Configure(config => config.AddGeneratedMappings()); -}); -``` - -The DI package registers `ImmutableFluentMapConfiguration` and `FluentMapRuntime` as singletons. It does not register database connections, repositories, Dommel bridges or global Dapper type maps. - -## AOT / Trimming - -FluentMap has partial trimming/AOT readiness, not full Native AOT compatibility: - -| Area | Status | -| --- | --- | -| Explicit registration with `AddMap()` | Preferred for trimming and Native AOT scenarios. | -| Generated registration with `AddGeneratedMappings()` | Preferred alternative to assembly scanning for maps in the current compilation. | -| `UseStrictGeneratedMaterialization()` with `QueryGeneratedMapped*` | Generated-only, parameterless-command path validated by the Native AOT smoke; unsupported result shapes fail deterministically instead of using runtime fallback. | -| Assembly scanning | Reflection-based and annotated as trimming-sensitive. | -| `QueryMapped*`, `ReadMapped*`, `QueryMultipleMapped`, streaming | Annotated as trimming/dynamic-code sensitive because runtime fallback can occur. | - -Do not treat the package as fully Native AOT safe unless your application validates the exact query path and deployment mode. +- the minimal migration path; +- Dapper and Dommel prerequisites; +- the `Ignore()`/Dommel persistence review; +- scenario-specific migration decisions; +- the migration checklist. ## Compatibility -Current compatibility documentation lives in [COMPATIBILITY.md](COMPATIBILITY.md). +Compatibility claims are intentionally kept outside the README so they can evolve without duplicating release-sensitive details. -Short version: +See [COMPATIBILITY.md](COMPATIBILITY.md) for: -- public packages target `netstandard2.0`; -- tests currently run on `net10.0`; -- Dapper range is `[2.1.79,3.0.0)`, with `2.1.79` validated in the current matrix; -- Dommel range is `[3.5.3,4.0.0)` for the optional Dommel package; -- SQLite is validated by automated provider tests; -- SQL Server 2022 CU23 and PostgreSQL 18.6 are certified by mandatory real-database CI lanes with `Microsoft.Data.SqlClient` 7.1.0 and `Npgsql` 10.0.3; -- MySQL 8.4.11 and MariaDB 11.8.9 are certified by mandatory real-database CI lanes with `MySqlConnector` 2.6.2; -- SQL Server CE remains legacy/upstream-limited. +- supported Dapper and Dommel ranges; +- provider certification; +- trimming and Native AOT boundaries; +- global-state limitations; +- unsupported environments and API boundaries. -For users moving from FluentMap 2.x, see [MIGRATION.md](MIGRATION.md). - -## Current Limitations - -- `FluentMapper.Initialize(...)`, normal `Dapper.Query()` and Dommel integrations use process-wide global state. -- Isolated runtimes apply to FluentMap-controlled materialization, not to normal Dapper queries or Dommel. -- Dommel uses global `DommelMapper` resolvers/builders. -- `QueryMultipleMapped` and `QueryMultipleMappedAsync` are sequential and buffered per result set. -- Two-type `QueryMapped` supports one row split with `splitOn`; it does not aggregate rows into one-to-many graphs. -- FluentMap does not aggregate joined rows into graphs or maintain identity maps. -- Write converters are metadata-only in the current Dapper/Dommel write path. -- Generated materializers support exact shapes and safe permutations of distinct columns; missing, additional or duplicate-column shapes fall back unless strict generated materialization is enabled. -- The Native AOT-certified `QueryGeneratedMapped*` subset accepts parameterless commands; dynamic parameter objects remain outside that strict contract. -- Assembly scanning and runtime fallback are trimming/AOT-sensitive. -- Value object construction uses compatible public constructors, not factory methods. - -## More Documentation +## Documentation +- [Usage guide](USAGE.md) - [Migration from 2.x](MIGRATION.md) - [Compatibility](COMPATIBILITY.md) -- [Support](SUPPORT.md) - [Changelog](CHANGELOG.md) +- [Support](SUPPORT.md) - [Português (Brasil)](README.pt-BR.md) ## Contributing -Keep changes small, compatible with the public API and covered by focused tests. `Dapper.FluentMap.slnx` is the preferred solution for current .NET SDKs; `Dapper.FluentMap.sln` remains available as a compatibility fallback. Typical local validation: +Keep changes small, compatible with the public API and covered by focused tests. `Dapper.FluentMap.slnx` is the preferred solution for current .NET SDKs; `Dapper.FluentMap.sln` remains available as a compatibility fallback. -When enabled, SonarQube Cloud is part of the CI quality gate, with results published at the [project dashboard](https://sonarcloud.io/summary/new_code?id=rodri-oliveira-dev_Dapper-FluentMap). Authorized SonarQube analysis requires the `SONAR_CI_ENABLED=true` repository variable, the `SONAR_TOKEN` repository secret and the project configured for CI-based analysis. +Typical local validation: ```bash dotnet restore ./Dapper.FluentMap.slnx @@ -523,6 +172,8 @@ dotnet build ./Dapper.FluentMap.slnx --configuration Release --no-restore dotnet test ./Dapper.FluentMap.slnx --configuration Release --no-build ``` +When enabled, SonarQube Cloud participates in the CI quality gate. Repository-specific CI configuration is intentionally kept out of this README. + ## License FluentMap is licensed under the [MIT License](LICENSE). diff --git a/README.pt-BR.md b/README.pt-BR.md index e836c09..5043d81 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -1,9 +1,12 @@ # FluentMap [![CI](https://github.com/rodri-oliveira-dev/Dapper-FluentMap/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/rodri-oliveira-dev/Dapper-FluentMap/actions/workflows/ci.yml) +[![CodeQL](https://github.com/rodri-oliveira-dev/Dapper-FluentMap/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/rodri-oliveira-dev/Dapper-FluentMap/actions/workflows/codeql.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=rodri-oliveira-dev_Dapper-FluentMap&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=rodri-oliveira-dev_Dapper-FluentMap) -[![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4?logo=dotnet&logoColor=white)](https://learn.microsoft.com/dotnet/standard/net-standard) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=rodri-oliveira-dev_Dapper-FluentMap&metric=coverage)](https://sonarcloud.io/summary/new_code?id=rodri-oliveira-dev_Dapper-FluentMap) +[![codecov](https://codecov.io/github/rodri-oliveira-dev/Dapper-FluentMap/branch/main/graph/badge.svg)](https://codecov.io/github/rodri-oliveira-dev/Dapper-FluentMap) +[![NuGet](https://img.shields.io/nuget/v/Dapper.FluentMap?logo=nuget)](https://www.nuget.org/packages/Dapper.FluentMap) +[![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4?logo=dotnet&logoColor=white)](https://learn.microsoft.com/dotnet/standard/net-standard) [![License: MIT](https://img.shields.io/github/license/rodri-oliveira-dev/Dapper-FluentMap)](LICENSE) [![GitHub stars](https://img.shields.io/github/stars/rodri-oliveira-dev/Dapper-FluentMap?style=flat&logo=github)](https://github.com/rodri-oliveira-dev/Dapper-FluentMap/stargazers) @@ -11,65 +14,43 @@ FluentMap é uma camada avançada de mapeamento para Dapper. Ela permite descrever, com uma API fluente e fortemente tipada, como propriedades .NET se conectam a colunas de banco de dados, mantendo atributos de persistência fora dos POCOs. -FluentMap não é um ORM. Ele não faz tracking de entidades, não gera SQL arbitrário, não gerencia conexões, não executa migrations, não oferece LINQ e não substitui o Dapper. Use FluentMap quando o mapeamento padrão por nome do Dapper não for suficiente e as regras de mapeamento precisarem ficar fora do modelo. +FluentMap não é um ORM. Ele não faz tracking de entidades, não gera SQL arbitrário, não gerencia conexões, não executa migrations, não oferece LINQ e não substitui o Dapper. ## Estado do Projeto -Dapper.FluentMap está sendo modernizado e mantido ativamente novamente. A versão 3.0 continua a história do projeto original, preservando o modelo central de mapeamento do FluentMap e seu caminho de compatibilidade. - -A linha 3.0 moderniza a biblioteca e adiciona novos recursos opt-in sem transformar FluentMap em um ORM e sem exigir que aplicações existentes adotem as novas APIs. - -## Voltando do FluentMap 2.x? - -Se você já usava FluentMap e está retornando ao projeto, os pontos mais importantes de compatibilidade são: - -- mappings existentes com `EntityMap` continuam suportados; -- `FluentMapper.Initialize(...)` continua suportado; -- chamadas normais de `Dapper.Query()` continuam funcionando com mappings raiz instalados pela API estática histórica; -- a maioria dos mappings raiz existentes não deve exigir mudanças de código; -- a maior parte dos recursos da 3.0 é opt-in, então não é necessário reescrever mappings que já funcionam apenas porque existem APIs novas. - -Consulte [MIGRATION.md](MIGRATION.md) para o caminho recomendado de migração da 2.x para a 3.0 e para as diferenças de comportamento que merecem revisão. +Dapper.FluentMap é mantido ativamente. A linha 3.x atual continua a história do projeto original, preservando o modelo histórico de mapping e adicionando recursos mais novos de forma opt-in. -## O que há de novo na 3.0 +Mappings existentes com `EntityMap` e `FluentMapper.Initialize(...)` continuam sendo a base de compatibilidade. Aplicações que usam mappings raiz normais do Dapper geralmente não precisam reescrever maps que já funcionam ao migrar da 2.x. -FluentMap 3.0 moderniza o projeto original sem mudar sua finalidade principal. Além da API fluente histórica, a linha 3.0 adiciona suporte opt-in para: +Consulte [MIGRATION.pt-BR.md](MIGRATION.pt-BR.md) ao migrar do FluentMap 2.x. -- melhorias no constructor mapping de tipos imutáveis; -- materialização de objetos aninhados e value objects; -- mapping profiles para formatos SQL alternativos; -- `QueryMultiple` mapeado, leituras unbuffered e streaming assíncrono; -- metadata de conversão de propriedades e diagnósticos; -- registro gerado de mappings e materializadores suportados; -- analyzers Roslyn para diagnósticos de mapping; -- configuração imutável isolada e dependency injection; -- metadata de persistência mais rica consumida pela integração Dommel; -- registro e diagnósticos conscientes de trimming/AOT; -- testes modernos de compatibilidade, harnesses de providers, benchmarks, CI e validação de pacotes. +## Principais Recursos -Mappings existentes continuam sendo a base de compatibilidade. Adote as APIs novas apenas quando elas resolverem um problema concreto. - -## Posicionamento - -Use FluentMap para: - -- mappings explícitos entre propriedades e colunas; -- convenções e políticas de nomenclatura; -- propriedades ignoradas; -- constructor mapping para tipos imutáveis; -- materialização opt-in de objetos aninhados e value objects; -- profiles para formatos SQL alternativos; -- registro e materialização gerados quando suportados; -- metadata de persistência consumida por integrações como Dommel; -- configuração isolada e DI para materialização controlada pelo FluentMap. - -Não use FluentMap como ORM, framework CRUD, query builder, unit of work ou abstração de banco. +| Recurso | API principal | +| --- | --- | +| Mapping explícito de propriedade para coluna | `EntityMap`, `Map(...).ToColumn(...)` | +| Convenções e naming policies | `AddConvention(...)`, `UseNamingPolicy(...)` | +| Constructor mapping imutável | bridge histórico de type map do Dapper | +| Objetos aninhados e value objects por componentes | `QueryMapped*` | +| Formatos SQL alternativos | mapping profiles | +| Multi-mapping de dois tipos | `QueryMapped(...)` | +| Múltiplos result sets | `QueryMultipleMapped*`, `ReadMapped*` | +| Streaming síncrono/assíncrono | `QueryMappedUnbuffered*` | +| Conversão por propriedade | property converters | +| Registro/materialização gerados | `AddGeneratedMappings()` | +| Caminho gerado estrito | `UseStrictGeneratedMaterialization()`, `QueryGeneratedMapped*` | +| Configuração isolada | `FluentMapRuntime` | +| Dependency Injection | `AddFluentMap(...)` | +| Metadata de persistência Dommel | `Dapper.FluentMap.Dommel` | +| Diagnósticos em compilação | `FluentMap.Analyzers` | + +Exemplos detalhados estão em [USAGE.pt-BR.md](USAGE.pt-BR.md). ## Instalação -Instale o pacote que corresponde ao recurso necessário: +Instale somente os pacotes necessários para a aplicação: -| Finalidade do pacote | NuGet PackageId | +| Finalidade | NuGet PackageId | | --- | --- | | Core | `Dapper.FluentMap` | | Integração Dommel | `Dapper.FluentMap.Dommel` | @@ -77,17 +58,15 @@ Instale o pacote que corresponde ao recurso necessário: | Analyzers Roslyn | `FluentMap.Analyzers` | | Source generators | `FluentMap.Generators` | +Pacote principal: + ```bash dotnet add package Dapper.FluentMap -dotnet add package Dapper.FluentMap.Dommel -dotnet add package FluentMap.DependencyInjection -dotnet add package FluentMap.Analyzers -dotnet add package FluentMap.Generators ``` -Os PackageIds `FluentMap.*` são apenas identidades de distribuição. Eles não renomeiam assemblies, namespaces C# ou APIs públicas existentes. +Os PackageIds `FluentMap.*` são apenas identidades de distribuição. Assemblies, namespaces e APIs públicas continuam sob `Dapper.FluentMap.*`. -Os pacotes públicos targetam `netstandard2.0`. Consulte [COMPATIBILITY.md](COMPATIBILITY.md) antes de adotar uma nova release. +Os pacotes públicos targetam `netstandard2.0`. Faixas suportadas de dependências e providers certificados estão documentados em [COMPATIBILITY.md](COMPATIBILITY.md). ## Início Rápido @@ -107,6 +86,7 @@ public sealed class CustomerMap : EntityMap public CustomerMap() { Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Name).ToColumn("customer_name"); } } @@ -116,406 +96,75 @@ FluentMapper.Initialize(config => }); var customer = connection.QuerySingle( - "SELECT 7 AS customer_id, 'Ada' AS Name;"); -``` - -Chame `FluentMapper.Initialize(...)` no startup e trate a configuração global efetiva como somente leitura depois que as queries começarem. - -## Mapeamento - -Crie maps herdando de `EntityMap`: - -```csharp -public sealed class ProductMap : EntityMap -{ - public ProductMap() - { - Map(product => product.Id).ToColumn("product_id"); - Map(product => product.Name).ToColumn("product_name", caseSensitive: false); - Map(product => product.TransientValue).Ignore(); - } -} -``` - -Mappings explícitos têm precedência sobre convenções. Membros raiz não mapeados usam o comportamento normal do Dapper. - -Convenções e políticas de nomenclatura cobrem padrões repetidos: - -```csharp -using Dapper.FluentMap.Conventions; -using Dapper.FluentMap.Naming; - -public sealed class PrefixConvention : Convention -{ - public PrefixConvention() - { - Properties().Configure(property => property.HasPrefix("col")); - } -} - -FluentMapper.Initialize(config => -{ - config.AddConvention().ForEntity(); - config.UseNamingPolicy(NamingPolicy.SnakeCase, caseSensitive: false) - .ForEntity(); -}); -``` - -As políticas disponíveis incluem `Identity`, `SnakeCase`, `Prefix(...)`, `Suffix(...)`, `Custom(...)`, `Then(...)`, `WithPrefix(...)` e `WithSuffix(...)`. - -## Tipos Imutáveis - -FluentMap participa do constructor mapping do Dapper para mappings explícitos no nível raiz: - -```csharp -public sealed class Customer -{ - public Customer(int id, string fullName) - { - Id = id; - FullName = fullName; - } - - public int Id { get; } - public string FullName { get; } -} - -public sealed class CustomerMap : EntityMap -{ - public CustomerMap() - { - Map(customer => customer.Id).ToColumn("customer_id"); - Map(customer => customer.FullName).ToColumn("full_name"); - } -} -``` - -Use `QueryMapped*` quando o FluentMap precisar construir objetos aninhados imutáveis ou value objects. - -## Objetos Aninhados - -Caminhos aninhados usam a mesma API `Map(...)`: - -```csharp -public sealed class CustomerMap : EntityMap -{ - public CustomerMap() - { - Map(customer => customer.Id).ToColumn("customer_id"); - Map(customer => customer.Address.City).ToColumn("city"); - } -} - -var customer = connection.QueryMappedSingle( - "SELECT 7 AS customer_id, 'Sao Paulo' AS city;"); + "SELECT 7 AS customer_id, 'Ada' AS customer_name;"); ``` -Materialização aninhada é opt-in via `QueryMapped*`, `ReadMapped*`, `QueryMultipleMapped` e helpers de streaming. `Dapper.Query()` normal continua usando materialização raiz do Dapper. - -## Value Objects +Chame `FluentMapper.Initialize(...)` durante o startup da aplicação e trate a configuração global efetiva como somente leitura depois que as queries começarem. -Para value objects escalares mapeados como um único valor de banco, prefira um `TypeHandler` do Dapper: +Para validar a configuração: ```csharp -Map(customer => customer.Cpf).ToColumn("cpf"); -``` - -Para value objects mapeados por componentes, a materialização controlada pelo FluentMap pode chamar construtores públicos compatíveis: - -```csharp -public sealed class CustomerMap : EntityMap -{ - public CustomerMap() - { - Map(customer => customer.Id).ToColumn("customer_id"); - Map(customer => customer.Cpf.Number).ToColumn("cpf"); - } -} - -var customer = connection.QueryMappedSingle( - "SELECT 1 AS customer_id, '12345678909' AS cpf;"); +FluentMapper.Validate(); ``` -Factory methods não são usadas pelo materializador atual. - -## Profiles +## Uso Avançado -Profiles são mappings opt-in para a mesma entidade em formatos SQL diferentes: - -```csharp -using Dapper.FluentMap.Mapping; - -public sealed class LegacyProfile : IMappingProfile -{ -} +Use as APIs de query controladas pelo FluentMap quando o mapping precisar de comportamento além do type map histórico no nível raiz do Dapper. -public sealed class LegacyCustomerMap : - EntityMap, - IProfileMap -{ - public LegacyCustomerMap() - { - Map(customer => customer.Id).ToColumn("id"); - Map(customer => customer.Name).ToColumn("legal_name"); - } -} +Isso inclui: -FluentMapper.Initialize(config => -{ - config.AddMap(); - config.AddProfile(); -}); +- objetos aninhados e value objects por componentes; +- profiles; +- multi-mapping de dois tipos com `splitOn`; +- múltiplos result sets mapeados; +- streaming síncrono/assíncrono; +- property converters; +- materialização gerada e gerada estrita; +- runtimes isolados e DI; +- metadata de persistência Dommel. -var legacy = connection.QueryMappedSingle( - "SELECT 7 AS id, 'Legacy Ltd.' AS legal_name;"); -``` +Consulte [USAGE.pt-BR.md](USAGE.pt-BR.md) para exemplos completos e orientação de API. -Profiles são selecionados por query controlada pelo FluentMap. Eles não substituem o type map global do Dapper para a entidade. +## Migrando da 2.x -## Materialização Gerada +A linha 3.x preserva o principal caminho histórico de mapping compatível em código-fonte. -Instale `FluentMap.Generators` para registro gerado de maps da compilação atual: +Se a aplicação usa `EntityMap`, `FluentMapper.Initialize(...)` e chamadas normais de `Dapper.Query()` para mappings raiz, a migração normalmente consiste em atualizar os pacotes, validar a configuração e executar os testes da aplicação. -```bash -dotnet add package FluentMap.Generators -``` +Consulte [MIGRATION.pt-BR.md](MIGRATION.pt-BR.md) para: -Depois chame a extensão gerada: - -```csharp -FluentMapper.Initialize(config => -{ - config.AddGeneratedMappings(); -}); -``` - -O generator emite chamadas `AddMap()` e `AddProfile()` para maps elegíveis. Para mappings explícitos suportados, ele também pode registrar materializadores de linha gerados para o shape ordenado de colunas, incluindo propriedades simples, caminhos aninhados, value objects construídos por construtor e read converters suportados estaticamente. - -Materialização gerada é otimização. Maps não suportados, shapes dinâmicos, divergências de shape, converters por instância/delegate e alguns padrões avançados usam fallback runtime. - -## Semântica de Persistência - -Metadata de persistência descreve participação em escrita sem mudar materialização de leitura: - -```csharp -Map(product => product.CreatedAt) - .ToColumn("created_at") - .DatabaseDefaultOnInsert(); - -Map(product => product.UpdatedAt) - .ToColumn("updated_at") - .ReadOnly(); - -Map(product => product.Total) - .ToColumn("total") - .Computed(); -``` - -`Ignore()` mantém o significado histórico: a propriedade não é materializada pelo FluentMap e não participa da metadata de persistência gerada. Para valores de banco que ainda devem ser selecionados, mas não escritos, use `ReadOnly()`, `Computed()`, `DatabaseDefaultOnInsert()`, `ExcludeFromInsert()` ou `ExcludeFromUpdate()`. - -O pacote core armazena metadata. Dommel é o pacote atual que a consome para comportamento de `INSERT` e `UPDATE` gerados. - -## QueryMultiple / Streaming - -Use os helpers de query do FluentMap quando a materialização precisa honrar nested mappings, value objects, profiles, converters ou materializers gerados: - -```csharp -var customers = connection.QueryMapped(sql); -var customer = connection.QueryMappedSingle(sql); -var legacy = connection.QueryMappedSingle(legacySql); -``` - -Para linhas de JOIN com duas entidades, use `splitOn` explícito e um delegate de composição: - -```csharp -var rows = connection.QueryMapped( - sql, - (customer, order) => new CustomerOrder(customer, order), - splitOn: "order_id"); -``` - -Cada segmento é materializado com seu próprio mapping FluentMap. Quando os perfis diferirem por segmento, use a sobrecarga de profiles, por exemplo `QueryMapped(...)`. Se todas as colunas do segundo segmento forem `NULL`, o segundo argumento será `null`, cobrindo a semântica comum de ausência de filho em `LEFT JOIN`. - -Para múltiplos result sets: - -```csharp -using var multi = connection.QueryMultipleMapped(sql); - -var customers = multi.ReadMapped(); -var orders = multi.ReadMapped(); -``` - -`ReadMapped*` consome result sets em sequência e bufferiza o result set atual. Leituras concorrentes no mesmo `MappedGridReader` não são suportadas; uma leitura concorrente falha deterministicamente com `InvalidOperationException`. Chamadores assíncronos podem usar as APIs async correspondentes: - -```csharp -await using var multi = await connection.QueryMultipleMappedAsync( - sql, - cancellationToken: cancellationToken); - -var customers = await multi.ReadMappedAsync(cancellationToken); -var orders = await multi.ReadMappedAsync(cancellationToken); -``` - -Para processamento incremental: - -```csharp -foreach (var customer in connection.QueryMappedUnbuffered(sql)) -{ - Process(customer); -} -``` - -Streaming assíncrono está disponível em `DbConnection`: - -```csharp -await foreach (var customer in connection.QueryMappedUnbufferedAsync( - sql, - cancellationToken)) -{ - await ProcessAsync(customer, cancellationToken); -} -``` - -Streaming mantém o reader subjacente aberto até a enumeração terminar ou o enumerator ser descartado. - -## Conversores de Propriedade - -Conversores de propriedade são configurados por propriedade mapeada e executam somente na materialização controlada pelo FluentMap: - -```csharp -public sealed class ProductMap : EntityMap -{ - public ProductMap() - { - Map(product => product.Status) - .ToColumn("status_code") - .ConvertFromDatabaseUsing(); - } -} - -public sealed class ProductStatusConverter : - IReadPropertyConverter -{ - public ProductStatus ConvertFromDatabase(string value) - { - return value == "A" ? ProductStatus.Active : ProductStatus.Inactive; - } -} -``` - -A precedência de conversão de leitura na materialização controlada pelo FluentMap é: - -```text -tratamento de null/DBNull - -> read converter da propriedade - -> Dapper TypeHandler - -> conversão default do FluentMap -``` - -Metadata de write converter pode ser configurada, mas não é executada atualmente por escritas Dapper ou Dommel. - -## Configuração Isolada / DI - -A API estática histórica continua suportada: - -```csharp -FluentMapper.Initialize(config => -{ - config.AddMap(); -}); -``` - -Para múltiplas configurações controladas pelo FluentMap no mesmo processo, crie configurações imutáveis e use seus runtimes: - -```csharp -using Dapper.FluentMap.Configuration; - -var runtime = new FluentMapConfigurationBuilder() - .AddMap() - .Build() - .CreateRuntime(); - -var customer = runtime.QueryMappedSingle( - connection, - "SELECT 7 AS customer_id, 'Ada' AS Name;"); -``` - -Instale `FluentMap.DependencyInjection` para registro em DI: - -```bash -dotnet add package FluentMap.DependencyInjection -``` - -```csharp -using Microsoft.Extensions.DependencyInjection; - -services.AddFluentMap(builder => -{ - builder.AddMap(); - builder.Configure(config => config.AddGeneratedMappings()); -}); -``` - -O pacote de DI registra `ImmutableFluentMapConfiguration` e `FluentMapRuntime` como singletons. Ele não registra conexões de banco, repositories, bridges Dommel ou type maps globais do Dapper. - -## AOT / Trimming - -FluentMap tem prontidão parcial para trimming/AOT, não compatibilidade Native AOT completa: - -| Área | Status | -| --- | --- | -| Registro explícito com `AddMap()` | Preferencial para cenários com trimming e Native AOT. | -| Registro gerado com `AddGeneratedMappings()` | Alternativa preferencial ao assembly scanning para maps da compilação atual. | -| `UseStrictGeneratedMaterialization()` com `QueryGeneratedMapped*` | Caminho exclusivamente gerado, para comandos sem parâmetros, validado pelo smoke Native AOT; shapes não suportados falham deterministicamente em vez de usar fallback runtime. | -| Assembly scanning | Baseado em reflection e anotado como sensível a trimming. | -| `QueryMapped*`, `ReadMapped*`, `QueryMultipleMapped`, streaming | Anotados como sensíveis a trimming/dynamic code porque fallback runtime pode ocorrer. | - -Não trate o pacote como totalmente seguro para Native AOT sem validar o caminho de query e o modo de publicação exatos da sua aplicação. +- o caminho mínimo de migração; +- os pré-requisitos de Dapper e Dommel; +- a revisão de persistência envolvendo `Ignore()`/Dommel; +- decisões de migração por cenário; +- o checklist final. ## Compatibilidade -A documentação atual de compatibilidade está em [COMPATIBILITY.md](COMPATIBILITY.md). +As afirmações de compatibilidade ficam intencionalmente fora do README para poderem evoluir sem duplicar informações sensíveis a cada release. -Resumo: +Consulte [COMPATIBILITY.md](COMPATIBILITY.md) para: -- pacotes públicos targetam `netstandard2.0`; -- testes rodam atualmente em `net10.0`; -- a faixa de Dapper é `[2.1.79,3.0.0)`, com `2.1.79` validado na matriz atual; -- a faixa de Dommel é `[3.5.3,4.0.0)` no pacote opcional Dommel; -- SQLite é validado por testes automatizados de provider; -- SQL Server 2022 CU23 e PostgreSQL 18.6 são certificados por lanes obrigatórias de CI com bancos reais usando `Microsoft.Data.SqlClient` 7.1.0 e `Npgsql` 10.0.3; -- MySQL 8.4.11 e MariaDB 11.8.9 são certificados por lanes obrigatórias de CI com bancos reais usando `MySqlConnector` 2.6.2; -- SQL Server CE permanece legado/limitado por upstream. +- faixas suportadas de Dapper e Dommel; +- certificação de providers; +- limites de trimming e Native AOT; +- limitações de estado global; +- ambientes não suportados e fronteiras de API. -Para migrar do FluentMap 2.x, consulte [MIGRATION.md](MIGRATION.md). +## Documentação -## Limitações Atuais - -- `FluentMapper.Initialize(...)`, `Dapper.Query()` normal e integrações Dommel usam estado global process-wide. -- Runtimes isolados se aplicam à materialização controlada pelo FluentMap, não a queries Dapper normais nem Dommel. -- Dommel usa resolvers/builders globais do `DommelMapper`. -- `QueryMultipleMapped` e `QueryMultipleMappedAsync` são sequenciais e bufferizados por result set. -- `QueryMapped` suporta split de uma linha em dois tipos com `splitOn`; ele não agrega linhas em grafos um-para-muitos. -- FluentMap não agrega linhas de joins em grafos e não mantém identity map. -- Write converters são apenas metadata no caminho atual de escrita Dapper/Dommel. -- Materializers gerados suportam shapes exatos e permutações seguras de colunas distintas; shapes com colunas ausentes, adicionais ou duplicadas usam fallback, exceto no modo gerado estrito. -- O subconjunto `QueryGeneratedMapped*` certificado em Native AOT aceita comandos sem parâmetros; objetos de parâmetros dinâmicos permanecem fora desse contrato estrito. -- Assembly scanning e fallback runtime são sensíveis a trimming/AOT. -- Construção de value objects usa construtores públicos compatíveis, não factory methods. - -## Mais Documentação - -- [Migração da 2.x](MIGRATION.md) +- [Guia de uso](USAGE.pt-BR.md) +- [Migração da 2.x](MIGRATION.pt-BR.md) - [Compatibilidade](COMPATIBILITY.md) -- [Suporte](SUPPORT.md) - [Changelog](CHANGELOG.md) +- [Suporte](SUPPORT.md) - [English](README.md) ## Contribuição -Mantenha mudanças pequenas, compatíveis com a API pública e cobertas por testes focados. `Dapper.FluentMap.slnx` é a solução preferencial para SDKs .NET atuais; `Dapper.FluentMap.sln` permanece disponível como fallback de compatibilidade. Validação local típica: +Mantenha mudanças pequenas, compatíveis com a API pública e cobertas por testes focados. `Dapper.FluentMap.slnx` é a solução preferencial para SDKs .NET atuais; `Dapper.FluentMap.sln` permanece disponível como fallback de compatibilidade. -Quando habilitado, SonarQube Cloud faz parte do quality gate da CI, com resultados publicados no [dashboard do projeto](https://sonarcloud.io/summary/new_code?id=rodri-oliveira-dev_Dapper-FluentMap). A análise autorizada do SonarQube exige a variável de repositório `SONAR_CI_ENABLED=true`, o secret de repositório `SONAR_TOKEN` e o projeto configurado para análise via CI. +Validação local típica: ```bash dotnet restore ./Dapper.FluentMap.slnx @@ -523,6 +172,8 @@ dotnet build ./Dapper.FluentMap.slnx --configuration Release --no-restore dotnet test ./Dapper.FluentMap.slnx --configuration Release --no-build ``` +Quando habilitado, SonarQube Cloud participa do quality gate da CI. A configuração específica do repositório fica intencionalmente fora deste README. + ## Licença FluentMap é licenciado sob a [MIT License](LICENSE). diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..a7a54ea --- /dev/null +++ b/USAGE.md @@ -0,0 +1,448 @@ +# FluentMap Usage Guide + +This guide contains practical examples for the maintained FluentMap 3.x line. + +For a short introduction and installation instructions, start with [README.md](README.md). For upgrades from FluentMap 2.x, see [MIGRATION.md](MIGRATION.md). For supported dependency ranges, providers and AOT boundaries, see [COMPATIBILITY.md](COMPATIBILITY.md). + +## Basic Mapping + +Create maps by deriving from `EntityMap`: + +```csharp +public sealed class ProductMap : EntityMap +{ + public ProductMap() + { + Map(product => product.Id).ToColumn("product_id"); + Map(product => product.Name).ToColumn("product_name", caseSensitive: false); + Map(product => product.TransientValue).Ignore(); + } +} +``` + +Register maps during application startup: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddMap(); +}); + +FluentMapper.Validate(); +``` + +Normal Dapper calls continue to use the global Dapper type-map bridge for root-level mappings: + +```csharp +var products = connection.Query(sql); +``` + +## Conventions And Naming Policies + +Repeated naming rules can be expressed with conventions: + +```csharp +using Dapper.FluentMap.Conventions; + +public sealed class PrefixConvention : Convention +{ + public PrefixConvention() + { + Properties().Configure(property => property.HasPrefix("col")); + } +} + +FluentMapper.Initialize(config => +{ + config.AddConvention().ForEntity(); +}); +``` + +Naming policies provide common transformations: + +```csharp +using Dapper.FluentMap.Naming; + +FluentMapper.Initialize(config => +{ + config.UseNamingPolicy(NamingPolicy.SnakeCase, caseSensitive: false) + .ForEntity(); +}); +``` + +Explicit mappings take precedence over conventions and naming policies. Unmapped root members fall back to Dapper's normal behavior. + +## Immutable Types + +FluentMap participates in Dapper constructor mapping for root-level explicit mappings: + +```csharp +public sealed class Customer +{ + public Customer(int id, string fullName) + { + Id = id; + FullName = fullName; + } + + public int Id { get; } + public string FullName { get; } +} + +public sealed class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.FullName).ToColumn("full_name"); + } +} +``` + +Use FluentMap-controlled query APIs when immutable construction also involves nested objects or value objects. + +## Nested Objects + +Nested member paths use the same `Map(...)` API: + +```csharp +public sealed class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Address.City).ToColumn("city"); + } +} + +var customer = connection.QueryMappedSingle( + "SELECT 7 AS customer_id, 'Sao Paulo' AS city;"); +``` + +Nested materialization is opt-in. Normal `Dapper.Query()` remains root-level materialization. + +## Value Objects + +For a value object stored as a single database value, prefer a Dapper `TypeHandler` when the representation is global for the type: + +```csharp +Map(customer => customer.Cpf).ToColumn("cpf"); +``` + +For value objects mapped through components, use FluentMap-controlled materialization: + +```csharp +public sealed class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Cpf.Number).ToColumn("cpf"); + } +} + +var customer = connection.QueryMappedSingle( + "SELECT 1 AS customer_id, '12345678909' AS cpf;"); +``` + +The current materializer uses compatible public constructors. Factory methods are not used. + +## Mapping Profiles + +Profiles let the same entity use alternate SQL shapes without replacing its default global Dapper type map: + +```csharp +public sealed class LegacyProfile : IMappingProfile +{ +} + +public sealed class LegacyCustomerMap : + EntityMap, + IProfileMap +{ + public LegacyCustomerMap() + { + Map(customer => customer.Id).ToColumn("id"); + Map(customer => customer.Name).ToColumn("legal_name"); + } +} + +FluentMapper.Initialize(config => +{ + config.AddMap(); + config.AddProfile(); +}); + +var customer = connection.QueryMappedSingle( + "SELECT 7 AS id, 'Legacy Ltd.' AS legal_name;"); +``` + +Profiles are selected per FluentMap-controlled query. + +## FluentMap-Controlled Queries + +Use `QueryMapped*` when materialization must honor nested mappings, value objects, profiles, converters or generated materializers: + +```csharp +var customers = connection.QueryMapped(sql); +var customer = connection.QueryMappedSingle(sql); +var legacy = connection.QueryMappedSingle(legacySql); +``` + +These APIs complement normal Dapper queries; they do not replace them for simple root-level mappings. + +## Two-Type Multi-Mapping + +For rows that contain two mapped entities, use explicit `splitOn` and a composition delegate: + +```csharp +var rows = connection.QueryMapped( + sql, + (customer, order) => new CustomerOrder(customer, order), + splitOn: "order_id"); +``` + +Each segment is materialized through FluentMap. Per-segment profile overloads are available when each segment needs a different profile. + +For common `LEFT JOIN` scenarios, if all columns in the second segment are `NULL`, the second argument can represent an absent child instead of forcing an invalid object. + +This API performs row splitting; it does not aggregate repeated rows into one-to-many object graphs. + +## Multiple Result Sets + +Use mapped multiple-result APIs when each result set needs FluentMap-controlled materialization: + +```csharp +using var multi = connection.QueryMultipleMapped(sql); + +var customers = multi.ReadMapped(); +var orders = multi.ReadMapped(); +``` + +Async variants are available: + +```csharp +await using var multi = await connection.QueryMultipleMappedAsync( + sql, + cancellationToken: cancellationToken); + +var customers = await multi.ReadMappedAsync(cancellationToken); +var orders = await multi.ReadMappedAsync(cancellationToken); +``` + +Result sets are consumed in order. Concurrent reads on the same `MappedGridReader` are rejected deterministically. + +## Streaming + +For incremental synchronous processing: + +```csharp +foreach (var customer in connection.QueryMappedUnbuffered(sql)) +{ + Process(customer); +} +``` + +Async streaming is available on `DbConnection`: + +```csharp +await foreach (var customer in connection.QueryMappedUnbufferedAsync( + sql, + cancellationToken)) +{ + await ProcessAsync(customer, cancellationToken); +} +``` + +Streaming keeps the underlying reader open until enumeration finishes or the enumerator is disposed. + +## Property Converters + +Property converters apply to a specific mapped property during FluentMap-controlled materialization: + +```csharp +public sealed class ProductMap : EntityMap +{ + public ProductMap() + { + Map(product => product.Status) + .ToColumn("status_code") + .ConvertFromDatabaseUsing(); + } +} + +public sealed class ProductStatusConverter : + IReadPropertyConverter +{ + public ProductStatus ConvertFromDatabase(string value) + { + return value == "A" + ? ProductStatus.Active + : ProductStatus.Inactive; + } +} +``` + +Read conversion precedence is: + +```text +null/DBNull handling + -> property read converter + -> Dapper TypeHandler + -> FluentMap default conversion +``` + +Normal `Dapper.Query()` does not execute property converters. Use Dapper `TypeHandler` for type-wide conversion. + +Write-converter metadata exists, but the current Dapper/Dommel write path does not execute it. + +## Persistence Metadata And Dommel + +Persistence metadata controls write participation while keeping values readable: + +```csharp +Map(product => product.CreatedAt) + .ToColumn("created_at") + .DatabaseDefaultOnInsert(); + +Map(product => product.UpdatedAt) + .ToColumn("updated_at") + .ReadOnly(); + +Map(product => product.Total) + .ToColumn("total") + .Computed(); +``` + +Semantics: + +| Mapping | Read | Insert | Update | +| --- | --- | --- | --- | +| default | yes | yes | yes | +| `Ignore()` | no | no | no | +| `ReadOnly()` | yes | no | no | +| `Computed()` | yes | no | no | +| `DatabaseDefaultOnInsert()` | yes | no | yes | +| `ExcludeFromInsert()` | yes | no | yes | +| `ExcludeFromUpdate()` | yes | yes | no | + +The core package stores the metadata. `Dapper.FluentMap.Dommel` consumes it for supported generated `INSERT` and `UPDATE` behavior. + +Configure Dommel through the historical global integration: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddMap(); + config.ForDommel(); +}); +``` + +`DommelEntityMap`, `IsKey()`, `IsIdentity()` and `SetGeneratedOption(...)` remain the Dommel-specific mapping surface. + +## Generated Registration And Materialization + +Install the source-generator package: + +```bash +dotnet add package FluentMap.Generators +``` + +Then use the generated registration extension: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddGeneratedMappings(); +}); +``` + +The generator discovers eligible maps in the current compilation and can register supported generated materializers. + +Generated materialization is normally an optimization: unsupported cases use the runtime fallback. + +## Strict Generated Materialization + +Applications that require a generated-only path can opt into strict generated materialization: + +```csharp +var runtime = new FluentMapConfigurationBuilder() + .Configure(config => config.AddGeneratedMappings()) + .UseStrictGeneratedMaterialization() + .Build() + .CreateRuntime(); + +var customer = runtime.QueryGeneratedMappedSingle( + connection, + "SELECT 7 AS customer_id, 'Ada' AS customer_name;"); +``` + +Unsupported shapes fail deterministically instead of silently using runtime materialization. + +The strict generated path has a narrower supported contract than normal FluentMap-controlled materialization. Review [COMPATIBILITY.md](COMPATIBILITY.md) before using it for trimming or Native AOT deployments. + +## Isolated Configuration + +Use immutable configuration and runtime instances when multiple FluentMap-controlled configurations must coexist in the same process: + +```csharp +using Dapper.FluentMap.Configuration; + +var runtime = new FluentMapConfigurationBuilder() + .AddMap() + .Build() + .CreateRuntime(); + +var customer = runtime.QueryMappedSingle( + connection, + "SELECT 7 AS customer_id, 'Ada' AS customer_name;"); +``` + +Isolation applies to FluentMap-controlled materialization. It does not isolate normal `Dapper.Query()` type maps or Dommel's process-wide configuration. + +## Dependency Injection + +Install: + +```bash +dotnet add package FluentMap.DependencyInjection +``` + +Register FluentMap: + +```csharp +using Microsoft.Extensions.DependencyInjection; + +services.AddFluentMap(builder => +{ + builder.AddMap(); + builder.Configure(config => config.AddGeneratedMappings()); +}); +``` + +The DI package registers `ImmutableFluentMapConfiguration` and `FluentMapRuntime` as singletons. It does not register database connections, repositories, Dommel integration or global Dapper type maps. + +## Analyzers + +Install compile-time mapping diagnostics with: + +```bash +dotnet add package FluentMap.Analyzers +``` + +Analyzers complement runtime validation. They do not execute mapping constructors, access databases or replace `FluentMapper.Validate()`. + +## Trimming And Native AOT + +FluentMap has trimming-aware APIs and a validated strict generated Native AOT smoke path, but full Native AOT compatibility is not claimed. + +Prefer explicit or generated registration over assembly scanning in trimming/AOT scenarios, and review [COMPATIBILITY.md](COMPATIBILITY.md) for the current supported boundary. + +## Related Documentation + +- [README](README.md) +- [Migration guide](MIGRATION.md) +- [Compatibility](COMPATIBILITY.md) +- [Changelog](CHANGELOG.md) +- [Support](SUPPORT.md) +- [Português (Brasil)](USAGE.pt-BR.md) diff --git a/USAGE.pt-BR.md b/USAGE.pt-BR.md new file mode 100644 index 0000000..d1ecd18 --- /dev/null +++ b/USAGE.pt-BR.md @@ -0,0 +1,448 @@ +# Guia de Uso do FluentMap + +Este guia reúne exemplos práticos para a linha FluentMap 3.x mantida atualmente. + +Para uma introdução curta e instruções de instalação, comece pelo [README.pt-BR.md](README.pt-BR.md). Para migração do FluentMap 2.x, consulte [MIGRATION.pt-BR.md](MIGRATION.pt-BR.md). Para faixas suportadas de dependências, providers e limites de AOT, consulte [COMPATIBILITY.md](COMPATIBILITY.md). + +## Mapping Básico + +Crie maps herdando de `EntityMap`: + +```csharp +public sealed class ProductMap : EntityMap +{ + public ProductMap() + { + Map(product => product.Id).ToColumn("product_id"); + Map(product => product.Name).ToColumn("product_name", caseSensitive: false); + Map(product => product.TransientValue).Ignore(); + } +} +``` + +Registre os maps durante o startup da aplicação: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddMap(); +}); + +FluentMapper.Validate(); +``` + +Chamadas normais do Dapper continuam usando o bridge global de type map para mappings no nível raiz: + +```csharp +var products = connection.Query(sql); +``` + +## Convenções e Naming Policies + +Regras repetidas de nomenclatura podem ser expressas com conventions: + +```csharp +using Dapper.FluentMap.Conventions; + +public sealed class PrefixConvention : Convention +{ + public PrefixConvention() + { + Properties().Configure(property => property.HasPrefix("col")); + } +} + +FluentMapper.Initialize(config => +{ + config.AddConvention().ForEntity(); +}); +``` + +Naming policies oferecem transformações comuns: + +```csharp +using Dapper.FluentMap.Naming; + +FluentMapper.Initialize(config => +{ + config.UseNamingPolicy(NamingPolicy.SnakeCase, caseSensitive: false) + .ForEntity(); +}); +``` + +Mappings explícitos têm precedência sobre conventions e naming policies. Membros raiz não mapeados usam o comportamento normal do Dapper. + +## Tipos Imutáveis + +FluentMap participa do constructor mapping do Dapper para mappings explícitos no nível raiz: + +```csharp +public sealed class Customer +{ + public Customer(int id, string fullName) + { + Id = id; + FullName = fullName; + } + + public int Id { get; } + public string FullName { get; } +} + +public sealed class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.FullName).ToColumn("full_name"); + } +} +``` + +Use APIs de query controladas pelo FluentMap quando a construção imutável também envolver objetos aninhados ou value objects. + +## Objetos Aninhados + +Caminhos aninhados usam a mesma API `Map(...)`: + +```csharp +public sealed class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Address.City).ToColumn("city"); + } +} + +var customer = connection.QueryMappedSingle( + "SELECT 7 AS customer_id, 'Sao Paulo' AS city;"); +``` + +A materialização aninhada é opt-in. `Dapper.Query()` normal continua fazendo materialização no nível raiz. + +## Value Objects + +Para um value object armazenado como um único valor do banco, prefira um `TypeHandler` do Dapper quando a representação for global para o tipo: + +```csharp +Map(customer => customer.Cpf).ToColumn("cpf"); +``` + +Para value objects mapeados por componentes, use materialização controlada pelo FluentMap: + +```csharp +public sealed class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Cpf.Number).ToColumn("cpf"); + } +} + +var customer = connection.QueryMappedSingle( + "SELECT 1 AS customer_id, '12345678909' AS cpf;"); +``` + +O materializador atual usa construtores públicos compatíveis. Factory methods não são utilizados. + +## Mapping Profiles + +Profiles permitem que a mesma entidade use formatos SQL alternativos sem substituir seu type map global padrão do Dapper: + +```csharp +public sealed class LegacyProfile : IMappingProfile +{ +} + +public sealed class LegacyCustomerMap : + EntityMap, + IProfileMap +{ + public LegacyCustomerMap() + { + Map(customer => customer.Id).ToColumn("id"); + Map(customer => customer.Name).ToColumn("legal_name"); + } +} + +FluentMapper.Initialize(config => +{ + config.AddMap(); + config.AddProfile(); +}); + +var customer = connection.QueryMappedSingle( + "SELECT 7 AS id, 'Legacy Ltd.' AS legal_name;"); +``` + +Profiles são selecionados por query controlada pelo FluentMap. + +## Queries Controladas pelo FluentMap + +Use `QueryMapped*` quando a materialização precisar respeitar nested mappings, value objects, profiles, converters ou materializadores gerados: + +```csharp +var customers = connection.QueryMapped(sql); +var customer = connection.QueryMappedSingle(sql); +var legacy = connection.QueryMappedSingle(legacySql); +``` + +Essas APIs complementam queries normais do Dapper; elas não substituem o Dapper para mappings simples no nível raiz. + +## Multi-Mapping de Dois Tipos + +Para linhas que contêm duas entidades mapeadas, use `splitOn` explícito e um delegate de composição: + +```csharp +var rows = connection.QueryMapped( + sql, + (customer, order) => new CustomerOrder(customer, order), + splitOn: "order_id"); +``` + +Cada segmento é materializado pelo FluentMap. Existem overloads de profile por segmento quando cada parte precisa de um profile diferente. + +Em cenários comuns com `LEFT JOIN`, se todas as colunas do segundo segmento forem `NULL`, o segundo argumento pode representar a ausência do filho sem forçar a criação de um objeto inválido. + +Essa API faz split de uma linha; ela não agrega linhas repetidas em grafos um-para-muitos. + +## Múltiplos Result Sets + +Use as APIs de múltiplos resultados mapeados quando cada result set precisar de materialização controlada pelo FluentMap: + +```csharp +using var multi = connection.QueryMultipleMapped(sql); + +var customers = multi.ReadMapped(); +var orders = multi.ReadMapped(); +``` + +Também existem variantes assíncronas: + +```csharp +await using var multi = await connection.QueryMultipleMappedAsync( + sql, + cancellationToken: cancellationToken); + +var customers = await multi.ReadMappedAsync(cancellationToken); +var orders = await multi.ReadMappedAsync(cancellationToken); +``` + +Os result sets são consumidos em ordem. Leituras concorrentes no mesmo `MappedGridReader` são rejeitadas deterministicamente. + +## Streaming + +Para processamento incremental síncrono: + +```csharp +foreach (var customer in connection.QueryMappedUnbuffered(sql)) +{ + Process(customer); +} +``` + +Streaming assíncrono está disponível em `DbConnection`: + +```csharp +await foreach (var customer in connection.QueryMappedUnbufferedAsync( + sql, + cancellationToken)) +{ + await ProcessAsync(customer, cancellationToken); +} +``` + +O streaming mantém o reader subjacente aberto até a enumeração terminar ou o enumerator ser descartado. + +## Property Converters + +Property converters se aplicam a uma propriedade mapeada específica durante a materialização controlada pelo FluentMap: + +```csharp +public sealed class ProductMap : EntityMap +{ + public ProductMap() + { + Map(product => product.Status) + .ToColumn("status_code") + .ConvertFromDatabaseUsing(); + } +} + +public sealed class ProductStatusConverter : + IReadPropertyConverter +{ + public ProductStatus ConvertFromDatabase(string value) + { + return value == "A" + ? ProductStatus.Active + : ProductStatus.Inactive; + } +} +``` + +A precedência de conversão de leitura é: + +```text +tratamento de null/DBNull + -> read converter da propriedade + -> Dapper TypeHandler + -> conversão default do FluentMap +``` + +`Dapper.Query()` normal não executa property converters. Use `TypeHandler` do Dapper para conversão global por tipo. + +Existe metadata de write converter, mas o caminho atual de escrita Dapper/Dommel não a executa. + +## Metadata de Persistência e Dommel + +Metadata de persistência controla a participação em escrita mantendo os valores disponíveis para leitura: + +```csharp +Map(product => product.CreatedAt) + .ToColumn("created_at") + .DatabaseDefaultOnInsert(); + +Map(product => product.UpdatedAt) + .ToColumn("updated_at") + .ReadOnly(); + +Map(product => product.Total) + .ToColumn("total") + .Computed(); +``` + +Semântica: + +| Mapping | Leitura | Insert | Update | +| --- | --- | --- | --- | +| padrão | sim | sim | sim | +| `Ignore()` | não | não | não | +| `ReadOnly()` | sim | não | não | +| `Computed()` | sim | não | não | +| `DatabaseDefaultOnInsert()` | sim | não | sim | +| `ExcludeFromInsert()` | sim | não | sim | +| `ExcludeFromUpdate()` | sim | sim | não | + +O pacote core armazena a metadata. `Dapper.FluentMap.Dommel` a consome para comportamentos suportados de `INSERT` e `UPDATE` gerados. + +Configure Dommel pela integração global histórica: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddMap(); + config.ForDommel(); +}); +``` + +`DommelEntityMap`, `IsKey()`, `IsIdentity()` e `SetGeneratedOption(...)` continuam sendo a superfície de mapping específica do Dommel. + +## Registro e Materialização Gerados + +Instale o pacote de source generator: + +```bash +dotnet add package FluentMap.Generators +``` + +Depois use a extensão de registro gerada: + +```csharp +FluentMapper.Initialize(config => +{ + config.AddGeneratedMappings(); +}); +``` + +O generator descobre maps elegíveis na compilação atual e pode registrar materializadores gerados suportados. + +Materialização gerada normalmente é uma otimização: casos não suportados usam fallback runtime. + +## Materialização Gerada Estrita + +Aplicações que exigem um caminho exclusivamente gerado podem habilitar materialização gerada estrita: + +```csharp +var runtime = new FluentMapConfigurationBuilder() + .Configure(config => config.AddGeneratedMappings()) + .UseStrictGeneratedMaterialization() + .Build() + .CreateRuntime(); + +var customer = runtime.QueryGeneratedMappedSingle( + connection, + "SELECT 7 AS customer_id, 'Ada' AS customer_name;"); +``` + +Shapes não suportados falham deterministicamente em vez de usar silenciosamente materialização runtime. + +O caminho gerado estrito possui um contrato suportado mais restrito do que a materialização normal controlada pelo FluentMap. Revise [COMPATIBILITY.md](COMPATIBILITY.md) antes de usá-lo em deployments com trimming ou Native AOT. + +## Configuração Isolada + +Use configuração imutável e instâncias de runtime quando múltiplas configurações controladas pelo FluentMap precisarem coexistir no mesmo processo: + +```csharp +using Dapper.FluentMap.Configuration; + +var runtime = new FluentMapConfigurationBuilder() + .AddMap() + .Build() + .CreateRuntime(); + +var customer = runtime.QueryMappedSingle( + connection, + "SELECT 7 AS customer_id, 'Ada' AS customer_name;"); +``` + +O isolamento se aplica à materialização controlada pelo FluentMap. Ele não isola os type maps de `Dapper.Query()` normal nem a configuração process-wide do Dommel. + +## Dependency Injection + +Instale: + +```bash +dotnet add package FluentMap.DependencyInjection +``` + +Registre o FluentMap: + +```csharp +using Microsoft.Extensions.DependencyInjection; + +services.AddFluentMap(builder => +{ + builder.AddMap(); + builder.Configure(config => config.AddGeneratedMappings()); +}); +``` + +O pacote de DI registra `ImmutableFluentMapConfiguration` e `FluentMapRuntime` como singletons. Ele não registra conexões de banco, repositories, integração Dommel ou type maps globais do Dapper. + +## Analyzers + +Instale diagnósticos de mapping em tempo de compilação com: + +```bash +dotnet add package FluentMap.Analyzers +``` + +Analyzers complementam a validação em runtime. Eles não executam construtores de mapping, não acessam bancos e não substituem `FluentMapper.Validate()`. + +## Trimming e Native AOT + +FluentMap possui APIs conscientes de trimming e um caminho de smoke Native AOT gerado estrito validado, mas compatibilidade Native AOT completa não é afirmada. + +Prefira registro explícito ou gerado em vez de assembly scanning em cenários com trimming/AOT e revise [COMPATIBILITY.md](COMPATIBILITY.md) para o limite de suporte atual. + +## Documentação Relacionada + +- [README](README.pt-BR.md) +- [Guia de migração](MIGRATION.pt-BR.md) +- [Compatibilidade](COMPATIBILITY.md) +- [Changelog](CHANGELOG.md) +- [Suporte](SUPPORT.md) +- [English](USAGE.md) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..11e74da --- /dev/null +++ b/codecov.yml @@ -0,0 +1,35 @@ +coverage: + precision: 2 + round: down + range: "80..100" + + status: + project: + default: + target: auto + threshold: 1% + + patch: + default: + target: 80% + threshold: 1% + +comment: + layout: "condensed_header, condensed_files, condensed_footer" + behavior: default + require_changes: false + require_base: false + require_head: true + hide_project_coverage: false + +github_checks: + annotations: true + +ignore: + - "test/**" + - "benchmarks/**" + - "eng/**" + - "**/bin/**" + - "**/obj/**" + - "**/*.g.cs" + - "**/*.generated.cs"