From d85b5d1b20e769c8c31bc72d269d890cb8d9b2e2 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Wed, 30 Sep 2026 03:53:25 -0300 Subject: [PATCH 1/9] fix: stabilize compatibility contract (#212) --- .github/workflows/ci.yml | 10 ++ COMPATIBILITY.md | 2 + Directory.Build.props | 2 +- MIGRATION.md | 2 +- eng/compatibility-contract.json | 8 ++ eng/test-compatibility-consistency.ps1 | 52 ++++++++ eng/validate-compatibility-consistency.ps1 | 126 ++++++++++++++++++ .../GeneratedRegistrationIntegrationTests.cs | 4 + .../ManualMappingTests.cs | 55 +++++++- 9 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 eng/compatibility-contract.json create mode 100644 eng/test-compatibility-consistency.ps1 create mode 100644 eng/validate-compatibility-consistency.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c43a626..6162f37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,10 @@ jobs: shell: pwsh run: ./eng/test-release-governance.ps1 + - name: Validate compatibility contract consistency + shell: pwsh + run: ./eng/test-compatibility-consistency.ps1 + - name: Validate Dependabot configuration run: check-jsonschema --builtin-schema vendor.dependabot .github/dependabot.yml @@ -376,6 +380,12 @@ jobs: $ErrorActionPreference = 'Stop' ./eng/validate-package-metadata.ps1 -PackageDirectory './artifacts/packages' + - name: Validate packaged compatibility contract + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + ./eng/validate-compatibility-consistency.ps1 -PackageDirectory './artifacts/packages' + - name: Validate package artifact set shell: pwsh run: | diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 4f290fd..7d2ed61 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -18,6 +18,8 @@ The `FluentMap.*` PackageIds are NuGet distribution identities. Assemblies and n ## Dapper +The machine-readable source of truth is `eng/compatibility-contract.json`. CI verifies this document, package properties, compatibility lanes, migration guidance and produced NuGet dependency metadata against that contract. + Current package range: ```text diff --git a/Directory.Build.props b/Directory.Build.props index 6552d28..dad4dd0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ 3.4.0 dev $(FluentMapPackageVersionSuffix) - 2.1.89 + 2.1.79 2.1.89 [$(DapperMinimumSupportedVersion),3.0.0) [3.5.3,4.0.0) diff --git a/MIGRATION.md b/MIGRATION.md index b4707f1..2740c3b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -53,7 +53,7 @@ Before changing FluentMap package versions: - 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. -Current supported package ranges are documented in [COMPATIBILITY.md](COMPATIBILITY.md). At the time of this guide: +Current supported package ranges are documented in [COMPATIBILITY.md](COMPATIBILITY.md). The Dapper values are governed by `eng/compatibility-contract.json` and validated against package metadata and CI. At the time of this guide: ```text Dapper [2.1.79,3.0.0) diff --git a/eng/compatibility-contract.json b/eng/compatibility-contract.json new file mode 100644 index 0000000..bbcf787 --- /dev/null +++ b/eng/compatibility-contract.json @@ -0,0 +1,8 @@ +{ + "schemaVersion": "1.0", + "dapper": { + "minimumSupportedVersion": "2.1.79", + "latestStableVersion": "2.1.89", + "packageRange": "[2.1.79,3.0.0)" + } +} diff --git a/eng/test-compatibility-consistency.ps1 b/eng/test-compatibility-consistency.ps1 new file mode 100644 index 0000000..475c956 --- /dev/null +++ b/eng/test-compatibility-consistency.ps1 @@ -0,0 +1,52 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +$validator = Join-Path $PSScriptRoot 'validate-compatibility-consistency.ps1' + +& $validator -RepositoryRoot $repoRoot + +$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("fluentmap-compatibility-" + [Guid]::NewGuid().ToString('N')) +try { + foreach ($directory in @('eng', '.github/workflows')) { + [System.IO.Directory]::CreateDirectory((Join-Path $temporaryRoot $directory)) | Out-Null + } + + foreach ($relativePath in @( + 'eng/compatibility-contract.json', + 'Directory.Build.props', + '.github/workflows/ci.yml', + 'COMPATIBILITY.md', + 'MIGRATION.md')) { + $destination = Join-Path $temporaryRoot $relativePath + [System.IO.File]::Copy((Join-Path $repoRoot $relativePath), $destination) + } + + $compatibilityPath = Join-Path $temporaryRoot 'COMPATIBILITY.md' + $drifted = [System.IO.File]::ReadAllText($compatibilityPath).Replace( + 'Dapper [2.1.79,3.0.0)', + 'Dapper [2.1.89,3.0.0)') + [System.IO.File]::WriteAllText($compatibilityPath, $drifted) + + $detected = $false + try { + & $validator -RepositoryRoot $temporaryRoot + } + catch { + $detected = $_.Exception.Message -like '*COMPATIBILITY.md is inconsistent*' + } + + if (-not $detected) { + throw 'Negative compatibility-consistency proof failed: documentation drift was not detected with an actionable file name.' + } + + Write-Host 'PASS compatibility consistency validator detects intentional documentation drift.' +} +finally { + if (Test-Path -LiteralPath $temporaryRoot) { + Remove-Item -LiteralPath $temporaryRoot -Recurse -Force + } +} diff --git a/eng/validate-compatibility-consistency.ps1 b/eng/validate-compatibility-consistency.ps1 new file mode 100644 index 0000000..a3d43d0 --- /dev/null +++ b/eng/validate-compatibility-consistency.ps1 @@ -0,0 +1,126 @@ +[CmdletBinding()] +param( + [string]$RepositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path, + [string]$PackageDirectory +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Fail { + param([string]$Message) + throw "Compatibility consistency validation failed: $Message" +} + +function Read-RequiredText { + param([string]$RelativePath) + + $path = Join-Path $RepositoryRoot $RelativePath + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + Fail "Required file '$RelativePath' was not found." + } + + return Get-Content -LiteralPath $path -Raw +} + +function Assert-Contains { + param( + [string]$RelativePath, + [string]$Text, + [string]$Expected, + [string]$ContractName + ) + + if ($Text.IndexOf($Expected, [System.StringComparison]::Ordinal) -lt 0) { + Fail "$RelativePath is inconsistent for $ContractName. Expected to find '$Expected'." + } +} + +$manifestPath = Join-Path $RepositoryRoot 'eng/compatibility-contract.json' +if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + Fail "The source-of-truth manifest 'eng/compatibility-contract.json' was not found." +} + +$contract = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ([string]$contract.schemaVersion -ne '1.0') { + Fail "Unsupported manifest schemaVersion '$($contract.schemaVersion)'. Expected '1.0'." +} + +$minimum = [string]$contract.dapper.minimumSupportedVersion +$latest = [string]$contract.dapper.latestStableVersion +$packageRange = [string]$contract.dapper.packageRange +$expectedRange = "[$minimum,3.0.0)" + +if ($packageRange -ne $expectedRange) { + Fail "eng/compatibility-contract.json declares Dapper packageRange '$packageRange', expected '$expectedRange' from minimumSupportedVersion." +} + +$props = Read-RequiredText 'Directory.Build.props' +Assert-Contains 'Directory.Build.props' $props "$minimum" 'Dapper minimum supported version' +Assert-Contains 'Directory.Build.props' $props "$latest" 'Dapper latest stable version' +Assert-Contains 'Directory.Build.props' $props '[$(DapperMinimumSupportedVersion),3.0.0)' 'Dapper package range derivation' + +$ci = Read-RequiredText '.github/workflows/ci.yml' +Assert-Contains '.github/workflows/ci.yml' $ci "dapper-version: $minimum" 'Dapper minimum compatibility lane' +Assert-Contains '.github/workflows/ci.yml' $ci "dapper-version: $latest" 'Dapper latest-stable compatibility lane' + +foreach ($document in @('COMPATIBILITY.md', 'MIGRATION.md')) { + $text = Read-RequiredText $document + Assert-Contains $document $text "Dapper $packageRange" 'documented Dapper package range' +} + +if (-not [string]::IsNullOrWhiteSpace($PackageDirectory)) { + if (-not (Test-Path -LiteralPath $PackageDirectory -PathType Container)) { + Fail "Package directory '$PackageDirectory' does not exist." + } + + Add-Type -AssemblyName System.IO.Compression.FileSystem + $packagesToValidate = @('Dapper.FluentMap', 'Dapper.FluentMap.Dommel') + foreach ($packageId in $packagesToValidate) { + $packagePattern = '^' + [Regex]::Escape($packageId) + '\.\d.*\.nupkg$' + $matches = @( + Get-ChildItem -LiteralPath $PackageDirectory -File -Filter '*.nupkg' | + Where-Object { $_.Name -match $packagePattern } + ) + if ($matches.Count -ne 1) { + Fail "Expected exactly one package for '$packageId' in '$PackageDirectory', found $($matches.Count)." + } + + $archive = [System.IO.Compression.ZipFile]::OpenRead($matches[0].FullName) + try { + $nuspecEntry = @($archive.Entries | Where-Object { $_.FullName -like '*.nuspec' }) + if ($nuspecEntry.Count -ne 1) { + Fail "$($matches[0].Name) must contain exactly one nuspec, found $($nuspecEntry.Count)." + } + + $stream = $nuspecEntry[0].Open() + try { + $reader = [System.IO.StreamReader]::new($stream) + try { + [xml]$nuspec = $reader.ReadToEnd() + } + finally { + $reader.Dispose() + } + } + finally { + $stream.Dispose() + } + + $dependency = $nuspec.SelectSingleNode("//*[local-name()='dependency' and @id='Dapper']") + if ($null -eq $dependency) { + Fail "$($matches[0].Name) does not declare its required Dapper dependency." + } + + $actualRange = [string]$dependency.GetAttribute('version') + if ($actualRange.Replace(' ', '') -ne $packageRange) { + Fail "$($matches[0].Name) declares Dapper range '$actualRange', expected '$packageRange'." + } + } + finally { + $archive.Dispose() + } + } +} + +Write-Host "Compatibility contract is consistent: Dapper minimum $minimum, latest stable $latest, package range $packageRange." diff --git a/test/Dapper.FluentMap.GeneratedRegistration.Tests/GeneratedRegistrationIntegrationTests.cs b/test/Dapper.FluentMap.GeneratedRegistration.Tests/GeneratedRegistrationIntegrationTests.cs index 504949a..ad2c1ac 100644 --- a/test/Dapper.FluentMap.GeneratedRegistration.Tests/GeneratedRegistrationIntegrationTests.cs +++ b/test/Dapper.FluentMap.GeneratedRegistration.Tests/GeneratedRegistrationIntegrationTests.cs @@ -66,6 +66,10 @@ public void GeneratedRegistrationShouldWorkWithDapperAndExistingMappingFeatures( Assert.Equal(8, internalCustomer.Id); Assert.Equal(9, derived.Id); Assert.Equal("Lovelace", derived.Name); + var inheritedId = FluentMapper.Explain() + .Members.Single(member => member.MemberPath == nameof(GeneratedBaseCustomer.Id)); + Assert.Equal(typeof(GeneratedBaseCustomer), inheritedId.PropertyInfo.DeclaringType); + Assert.Equal(typeof(GeneratedBaseCustomer), inheritedId.PropertyInfo.ReflectedType); Assert.Equal(10, immutable.Id); Assert.Equal("Grace", immutable.Name); Assert.Equal(12, queryMappedImmutable.Id); diff --git a/test/Dapper.FluentMap.Tests/ManualMappingTests.cs b/test/Dapper.FluentMap.Tests/ManualMappingTests.cs index 9084e45..eb9431f 100644 --- a/test/Dapper.FluentMap.Tests/ManualMappingTests.cs +++ b/test/Dapper.FluentMap.Tests/ManualMappingTests.cs @@ -122,7 +122,7 @@ public void FluentMapperInitializeShouldAddDapperTypeMap() } [Fact] - public void PropertyMapShouldMapInheritedProperies() + public void InheritedPropertyShouldKeepDeclaringIdentityAndDerivedMapContext() { // Arrange PreTest(); @@ -132,10 +132,19 @@ public void PropertyMapShouldMapInheritedProperies() var idMap = map.PropertyMaps.First(); var nameMap = map.PropertyMaps.Skip(1).First(); - // Assert - // todo: should be ReflectedType so the type is DerivedTestEntity + // Assert: expression metadata identifies the member declared by the base type. + // The EntityMap supplies the derived mapping context; reflection does not + // rewrite either DeclaringType or ReflectedType to the expression parameter type. Assert.Equal(typeof(TestEntity), idMap.PropertyInfo.DeclaringType); + Assert.Equal(typeof(TestEntity), idMap.PropertyInfo.ReflectedType); Assert.Equal(typeof(DerivedTestEntity), nameMap.PropertyInfo.DeclaringType); + Assert.Equal(typeof(DerivedTestEntity), nameMap.PropertyInfo.ReflectedType); + + FluentMapper.Initialize(c => c.AddMap(map)); + var inheritedMember = SqlMapper.GetTypeMap(typeof(DerivedTestEntity)).GetMember("intId"); + + Assert.NotNull(inheritedMember); + Assert.Equal(typeof(TestEntity).GetProperty(nameof(TestEntity.Id)), inheritedMember.Property); } [Fact] @@ -158,6 +167,22 @@ public void PropertyMapShouldDistinguishNestedPropertiesWithSameTerminalName() Assert.Equal(2, map.PropertyMaps.Count); } + [Fact] + public void InheritedAndNestedMemberPathsWithSameTerminalNameShouldNotCollide() + { + FluentMapper.Reset(typeof(InheritedPathEntity)); + + var map = new InheritedPathMap(); + + Assert.Equal(2, map.PropertyMaps.Count); + + FluentMapper.Initialize(configuration => configuration.AddMap(map)); + var explanation = FluentMapper.Explain(); + + Assert.Contains(explanation.Members, member => member.MemberPath == "Id" && member.ColumnName == "entity_id"); + Assert.Contains(explanation.Members, member => member.MemberPath == "Details.Id" && member.ColumnName == "details_id"); + } + [Fact] public void DuplicateNestedPropertyPathShouldThrow_Exception() { @@ -244,6 +269,30 @@ public DuplicateNestedLevelMap() } } + private class InheritedPathBase + { + public int Id { get; set; } + } + + private class InheritedPathEntity : InheritedPathBase + { + public InheritedPathDetails Details { get; set; } + } + + private class InheritedPathDetails + { + public int Id { get; set; } + } + + private class InheritedPathMap : EntityMap + { + public InheritedPathMap() + { + Map(entity => entity.Id).ToColumn("entity_id"); + Map(entity => entity.Details.Id).ToColumn("details_id"); + } + } + private class NestedLevelEntity { public RankInfo Rank { get; set; } From 361ffdf2d6deb670532b9f51150b7a435da875cf Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Wed, 30 Sep 2026 04:32:30 -0300 Subject: [PATCH 2/9] feat: expand strict generated materialization (#213) --- CHANGELOG.md | 8 + COMPATIBILITY.md | 6 +- MIGRATION.md | 2 + USAGE.md | 9 +- .../Dapper.FluentMap.Benchmarks/Program.cs | 16 ++ .../MappingRegistrationGenerator.cs | 37 ++- src/Dapper.FluentMap/FluentMapRuntime.cs | 40 ++- src/Dapper.FluentMap/GeneratedParameters.cs | 109 +++++++++ src/Dapper.FluentMap/MappingRegistry.cs | 170 ++++++++++--- .../Materialization/MappedRowMaterializer.cs | 2 +- src/Dapper.FluentMap/QueryMappedExtensions.cs | 3 + test/Dapper.FluentMap.AotSmoke/Program.cs | 51 ++++ .../GeneratedRegistrationIntegrationTests.cs | 20 ++ .../MappingRegistrationGeneratorTests.cs | 23 +- .../ProviderCompatibilityTests.cs | 41 ++++ .../AdvancedQueryHardeningTests.cs | 6 +- .../GeneratedMaterializerContractTests.cs | 227 +++++++++++++++++- 17 files changed, 708 insertions(+), 62 deletions(-) create mode 100644 src/Dapper.FluentMap/GeneratedParameters.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e1651de..0089526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The historical archived package history is not reconstructed here. This changelo ## [Unreleased] +### Added + +- Added reflection-free `GeneratedParameters` for parameterized strict-generated queries, including provider-matrix and Native AOT smoke coverage. + +### Changed + +- Generated materializers now accept safe additional unmapped reader columns and provide specific strict diagnostics for missing, duplicate/ambiguous and mapped additional columns. + ## [3.4.0] - 2026-09-27 ### Added diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 7d2ed61..e33f394 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -77,14 +77,16 @@ Current status: | --- | --- | | Explicit map registration | Preferred for trimmed and Native AOT applications. | | Generated registration | Preferred alternative to assembly scanning for maps in the current compilation. | -| Strict generated runtime | `UseStrictGeneratedMaterialization()` and `QueryGeneratedMapped*` provide a generated-only path for parameterless commands; unsupported shapes and dynamic parameter objects fail deterministically. | +| Strict generated runtime | `UseStrictGeneratedMaterialization()` and `QueryGeneratedMapped*` provide a generated-only path for parameterless commands and explicitly typed `GeneratedParameters`; unsupported shapes and arbitrary parameter objects fail deterministically. | | Assembly scanning | Reflection-based and annotated as trimming-sensitive. | | `QueryMapped*`, `ReadMapped*`, `QueryMultipleMapped`, streaming | Annotated with trimming/dynamic-code warnings because runtime fallback can occur. | | Full Native AOT compatibility | Not claimed. | Trimmed smoke tests cover explicit, generated and DI scenarios. The CI Native AOT lane publishes and runs the strict generated SQLite smoke on `windows-latest`/`win-x64`, treating relevant trimming and AOT warnings as errors. Local Native AOT publishing still requires the Visual C++ linker toolchain and full Native AOT compatibility is not claimed. -Generated materializers accept their exact registered shape and safe permutations when column names are distinct. Missing, additional or duplicate-column shapes remain unsupported and use runtime fallback unless strict generated materialization is enabled. Resolution is based on reader metadata, not SQL parsing. +Generated materializers accept their exact registered shape, safe permutations and additional reader columns that do not resolve to mapped members. Missing required columns, duplicate/ambiguous names and additional columns that would map to an entity member remain unsupported and use runtime fallback unless strict generated materialization is enabled. Resolution is based on reader metadata, not SQL parsing. + +`GeneratedParameters` uses only public ADO.NET parameter contracts and requires an explicit `DbType` for every input. It does not inspect parameter-object members or invoke Dapper's anonymous-object parameter generation. The Native AOT smoke covers generated startup registration, parameter binding, reordered columns, a safe additional column and deterministic strict rejection. This is bounded evidence for the strict path, not a claim of full FluentMap Native AOT compatibility. ## Global State Limitations diff --git a/MIGRATION.md b/MIGRATION.md index 2740c3b..fa5d0da 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -352,6 +352,8 @@ This can replace manual registration for eligible maps in the current compilatio Generated materializers are an optimization. Unsupported cases fall back to runtime materialization unless strict generated materialization is explicitly enabled. +Strict generated queries can use `GeneratedParameters` for parameterized commands. Each value has an explicit `DbType`; anonymous objects and other arbitrary parameter bags remain unsupported so strict mode does not introduce reflection-based parameter discovery. Safe reordered and unmapped additional result columns can stay on the generated path, while missing, duplicate/ambiguous or mapped additional columns fail deterministically. + ## Configuration Isolation If your application needs multiple FluentMap configurations in the same process, use immutable configuration and runtime instances: diff --git a/USAGE.md b/USAGE.md index a7a54ea..d72d5d9 100644 --- a/USAGE.md +++ b/USAGE.md @@ -374,10 +374,15 @@ var runtime = new FluentMapConfigurationBuilder() var customer = runtime.QueryGeneratedMappedSingle( connection, - "SELECT 7 AS customer_id, 'Ada' AS customer_name;"); + "SELECT @Id AS customer_id, @Name AS customer_name, 'trace' AS trace_id;", + new GeneratedParameters() + .Add("Id", 7, System.Data.DbType.Int32) + .Add("Name", "Ada", System.Data.DbType.String, size: 100)); ``` -Unsupported shapes fail deterministically instead of silently using runtime materialization. +`GeneratedParameters` binds only explicitly named input values with an explicit `DbType`; it does not inspect anonymous objects. Parameterless commands remain supported. Arbitrary parameter objects fail before the connection is opened, with `QueryMapped*` identified as the non-strict alternative. + +Generated materializers accept reordered required columns and additional columns only when the additional names do not resolve to mapped members. Missing required columns, duplicate names, mapped additional columns, incompatible provider values and unsupported nested 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. diff --git a/benchmarks/Dapper.FluentMap.Benchmarks/Program.cs b/benchmarks/Dapper.FluentMap.Benchmarks/Program.cs index 87a1cd0..100ab80 100644 --- a/benchmarks/Dapper.FluentMap.Benchmarks/Program.cs +++ b/benchmarks/Dapper.FluentMap.Benchmarks/Program.cs @@ -31,6 +31,7 @@ public class MaterializationSteadyStateBenchmarks private SqliteConnection _connection = null!; private FluentMapRuntime _runtime = null!; + private FluentMapRuntime _strictRuntime = null!; [GlobalSetup] public async Task GlobalSetup() @@ -47,6 +48,11 @@ public async Task GlobalSetup() _runtime = new FluentMapRuntime(new FluentMapConfigurationBuilder() .Configure(configuration => configuration.AddGeneratedMappings()) .Build()); + _strictRuntime = new FluentMapConfigurationBuilder() + .Configure(configuration => configuration.AddGeneratedMappings()) + .UseStrictGeneratedMaterialization() + .Build() + .CreateRuntime(); _connection = OpenPopulatedConnection(); DapperPure(); @@ -61,6 +67,7 @@ public async Task GlobalSetup() await RuntimeQueryMappedSimpleUnbufferedAsync(); QueryMappedSimpleReorderedGenerated(); RuntimeQueryMappedSimpleReorderedGenerated(); + RuntimeQueryGeneratedSimpleExtraColumn(); QueryMappedSimpleUnbufferedReorderedGenerated(); await QueryMappedSimpleUnbufferedAsyncReorderedGenerated(); QueryMappedImmutableConstructor(); @@ -187,6 +194,15 @@ public int RuntimeQueryMappedSimpleReorderedGenerated() .Count(); } + [Benchmark] + public int RuntimeQueryGeneratedSimpleExtraColumn() + { + return _strictRuntime.QueryGeneratedMapped( + _connection, + "SELECT 'trace' AS trace_id, Name AS full_name, Id AS customer_id, Age AS customer_age, Balance AS account_balance, CreatedAt AS created_at FROM BenchmarkRows;") + .Count(); + } + [Benchmark] public int QueryMappedSimpleUnbufferedReorderedGenerated() { diff --git a/src/Dapper.FluentMap.Generators/MappingRegistrationGenerator.cs b/src/Dapper.FluentMap.Generators/MappingRegistrationGenerator.cs index f24597b..b4f0148 100644 --- a/src/Dapper.FluentMap.Generators/MappingRegistrationGenerator.cs +++ b/src/Dapper.FluentMap.Generators/MappingRegistrationGenerator.cs @@ -752,6 +752,18 @@ private static void AppendReadExpression( builder.Append(leaf.TypeName); builder.Append(">(record, "); builder.Append(leaf.Ordinal.ToString(System.Globalization.CultureInfo.InvariantCulture)); + builder.Append(", "); + builder.Append(EscapeStringLiteral(materializer.EntityTypeName)); + builder.Append(", "); + builder.Append(materializer.ProfileTypeName == null + ? "null" + : EscapeStringLiteral(materializer.ProfileTypeName)); + builder.Append(", "); + builder.Append(EscapeStringLiteral(leaf.MemberPath)); + builder.Append(", "); + builder.Append(EscapeStringLiteral(leaf.ColumnName)); + builder.Append(", "); + builder.Append(EscapeStringLiteral(leaf.TypeName)); builder.Append(')'); return; } @@ -822,7 +834,14 @@ private static string FormatGeneratedColumns(GeneratedMaterializationNode node) private static void AppendReadHelper(StringBuilder builder) { builder.AppendLine(); - builder.AppendLine(" private static T Read(global::System.Data.IDataRecord record, int ordinal)"); + builder.AppendLine(" private static T Read("); + builder.AppendLine(" global::System.Data.IDataRecord record,"); + builder.AppendLine(" int ordinal,"); + builder.AppendLine(" string entityTypeName,"); + builder.AppendLine(" string profileTypeName,"); + builder.AppendLine(" string memberPath,"); + builder.AppendLine(" string columnName,"); + builder.AppendLine(" string targetTypeName)"); builder.AppendLine(" {"); builder.AppendLine(" if (record.IsDBNull(ordinal))"); builder.AppendLine(" {"); @@ -830,7 +849,21 @@ private static void AppendReadHelper(StringBuilder builder) builder.AppendLine(" }"); builder.AppendLine(); builder.AppendLine(" var value = record.GetValue(ordinal);"); - builder.AppendLine(" return ConvertValue(value);"); + builder.AppendLine(" try"); + builder.AppendLine(" {"); + builder.AppendLine(" return ConvertValue(value);"); + builder.AppendLine(" }"); + builder.AppendLine(" catch (global::System.Exception exception) when (!(exception is global::Dapper.FluentMap.FluentMapConfigurationException))"); + builder.AppendLine(" {"); + builder.AppendLine(" var profileContext = profileTypeName == null"); + builder.AppendLine(" ? string.Empty"); + builder.AppendLine(" : \" Profile: '\" + profileTypeName + \"'.\";"); + builder.AppendLine(" throw new global::Dapper.FluentMap.FluentMapConfigurationException("); + builder.AppendLine(" \"Generated materialization failed for entity '\" + entityTypeName + \"'.\" + profileContext +"); + builder.AppendLine(" \" Member path: '\" + memberPath + \"'. Column: '\" + columnName + \"'. Provider CLR type: '\" + value.GetType().FullName +"); + builder.AppendLine(" \"'. Target type: '\" + targetTypeName + \"'. Strict generated materialization does not use runtime reflection fallback.\","); + builder.AppendLine(" exception);"); + builder.AppendLine(" }"); builder.AppendLine(" }"); builder.AppendLine(); builder.AppendLine(" private static TTarget ReadConverted("); diff --git a/src/Dapper.FluentMap/FluentMapRuntime.cs b/src/Dapper.FluentMap/FluentMapRuntime.cs index 0aac069..c792fba 100644 --- a/src/Dapper.FluentMap/FluentMapRuntime.cs +++ b/src/Dapper.FluentMap/FluentMapRuntime.cs @@ -273,7 +273,7 @@ public TEntity QueryMappedSingle< /// The entity type to materialize. /// The database connection. /// The SQL query to execute. - /// Optional query parameters. + /// Optional . Arbitrary parameter objects are rejected before command execution. /// Optional transaction. /// Optional command timeout. /// Optional command type. @@ -281,6 +281,7 @@ public TEntity QueryMappedSingle< /// /// Thrown when the runtime cannot resolve a generated materializer for the entity, profile and result-column shape. /// + /// Thrown when is not or a instance. public IEnumerable QueryGeneratedMapped( IDbConnection connection, string sql, @@ -295,14 +296,12 @@ public IEnumerable QueryGeneratedMapped( throw new ArgumentNullException(nameof(sql)); } - if (param != null) - { - throw new NotSupportedException("Strict generated materialization does not support dynamic parameter objects. Use a parameterless command or a non-strict query API."); - } + var generatedParameters = ValidateGeneratedParameters(param, profileType: null); return QueryMappedExtensions.ExecuteGeneratedMapped( connection, sql, + generatedParameters, transaction, commandTimeout, commandType, @@ -317,7 +316,7 @@ public IEnumerable QueryGeneratedMapped( /// The mapping profile marker type to use. /// The database connection. /// The SQL query to execute. - /// Optional query parameters. + /// Optional . Arbitrary parameter objects are rejected before command execution. /// Optional transaction. /// Optional command timeout. /// Optional command type. @@ -325,6 +324,7 @@ public IEnumerable QueryGeneratedMapped( /// /// Thrown when the runtime cannot resolve a generated materializer for the entity, profile and result-column shape. /// + /// Thrown when is not or a instance. public IEnumerable QueryGeneratedMapped( IDbConnection connection, string sql, @@ -340,14 +340,12 @@ public IEnumerable QueryGeneratedMapped( throw new ArgumentNullException(nameof(sql)); } - if (param != null) - { - throw new NotSupportedException("Strict generated materialization does not support dynamic parameter objects. Use a parameterless command or a non-strict query API."); - } + var generatedParameters = ValidateGeneratedParameters(param, typeof(TProfile)); return QueryMappedExtensions.ExecuteGeneratedMapped( connection, sql, + generatedParameters, transaction, commandTimeout, commandType, @@ -386,6 +384,28 @@ public TEntity QueryGeneratedMappedSingle( return QueryGeneratedMapped(connection, sql, param, transaction, commandTimeout, commandType).Single(); } + private static GeneratedParameters ValidateGeneratedParameters(object param, Type profileType) + where TEntity : class + { + if (param == null) + { + return null; + } + + if (param is GeneratedParameters generatedParameters) + { + return generatedParameters; + } + + var profileContext = profileType == null + ? "default profile" + : $"profile '{profileType.FullName}'"; + throw new NotSupportedException( + $"Strict generated query parameters for entity '{typeof(TEntity).FullName}' ({profileContext}) must use '{typeof(GeneratedParameters).FullName}'. " + + $"Parameter object type '{param.GetType().FullName}' is unsupported because strict mode does not discover parameters through reflection or dynamic code. " + + "Use GeneratedParameters with an explicit DbType for every value, or use QueryMapped* for the non-strict runtime path."); + } + /// /// Creates a lazy unbuffered query that materializes rows using this runtime. /// diff --git a/src/Dapper.FluentMap/GeneratedParameters.cs b/src/Dapper.FluentMap/GeneratedParameters.cs new file mode 100644 index 0000000..e0022a9 --- /dev/null +++ b/src/Dapper.FluentMap/GeneratedParameters.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Data; + +namespace Dapper.FluentMap +{ + /// + /// Provides explicitly described command parameters for strict generated queries without + /// discovering members from an arbitrary parameter object. + /// + /// + /// Each parameter requires an explicit . This keeps parameter binding + /// deterministic and suitable for the repository's supported Native AOT strict-generated path. + /// + public sealed class GeneratedParameters + { + private readonly List _parameters = new List(); + private readonly HashSet _names = new HashSet(StringComparer.OrdinalIgnoreCase); + + /// + /// Adds one input parameter. + /// + /// The parameter name, with or without a common provider prefix. + /// The provider parameter value. is sent as . + /// The database type used when binding the parameter. + /// The optional provider parameter size. + /// This parameter collection. + /// Thrown when is empty or duplicated, or is negative. + public GeneratedParameters Add(string name, object value, DbType dbType, int? size = null) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Parameter name cannot be null, empty or whitespace.", nameof(name)); + } + + var normalizedName = NormalizeName(name); + if (normalizedName.Length == 0) + { + throw new ArgumentException("Parameter name must contain a name after its provider prefix.", nameof(name)); + } + + if (size < 0) + { + throw new ArgumentException("Parameter size cannot be negative.", nameof(size)); + } + + if (!_names.Add(normalizedName)) + { + throw new ArgumentException($"Parameter '{name}' is already registered.", nameof(name)); + } + + _parameters.Add(new ParameterValue(normalizedName, value, dbType, size)); + return this; + } + + internal void AddParameters(IDbCommand command) + { + if (command == null) + { + throw new ArgumentNullException(nameof(command)); + } + + foreach (var configured in _parameters) + { + var parameter = command.CreateParameter(); + parameter.ParameterName = configured.Name; + parameter.Value = configured.Value ?? DBNull.Value; + parameter.DbType = configured.DbType; + parameter.Direction = ParameterDirection.Input; + if (configured.Size.HasValue) + { + parameter.Size = configured.Size.Value; + } + + command.Parameters.Add(parameter); + } + } + + private static string NormalizeName(string name) + { + var trimmed = name.Trim(); + if (trimmed.Length > 0 && (trimmed[0] == '@' || trimmed[0] == ':' || trimmed[0] == '?')) + { + return trimmed.Substring(1); + } + + return trimmed; + } + + private sealed class ParameterValue + { + internal ParameterValue(string name, object value, DbType dbType, int? size) + { + Name = name; + Value = value; + DbType = dbType; + Size = size; + } + + internal string Name { get; } + + internal object Value { get; } + + internal DbType DbType { get; } + + internal int? Size { get; } + } + } +} diff --git a/src/Dapper.FluentMap/MappingRegistry.cs b/src/Dapper.FluentMap/MappingRegistry.cs index 1ae526c..088e2f3 100644 --- a/src/Dapper.FluentMap/MappingRegistry.cs +++ b/src/Dapper.FluentMap/MappingRegistry.cs @@ -348,6 +348,12 @@ internal bool TryGetGeneratedMaterializer( EnsureProfileRegistered(type, profileType); + if (HasDuplicateColumnNames(columnNames)) + { + materializer = null; + return false; + } + var cacheKey = new MaterializationPlanCacheKey(type, profileType, columnNames); GeneratedMaterializerEntry entry; if (_generatedMaterializers.TryGetValue(cacheKey, out entry) && @@ -363,7 +369,12 @@ internal bool TryGetGeneratedMaterializer( return true; } - if (TryCreateReorderedGeneratedMaterializer(type, profileType, columnNames, out entry)) + if (TryCreateCompatibleGeneratedMaterializer( + type, + profileType, + columnNames, + requireExplicitMapping: false, + out entry)) { _generatedMaterializerResolutionCache.TryAdd(cacheKey, entry); materializer = entry.Materialize; @@ -381,6 +392,12 @@ internal bool TryGetStrictGeneratedMaterializer( out Func materializer) { EnsureProfileRegistered(type, profileType); + if (HasDuplicateColumnNames(columnNames)) + { + materializer = null; + return false; + } + var cacheKey = new MaterializationPlanCacheKey(type, profileType, columnNames); if (_generatedMaterializers.TryGetValue(cacheKey, out var entry) && @@ -390,25 +407,15 @@ internal bool TryGetStrictGeneratedMaterializer( return true; } - if (!HasDuplicateColumnNames(columnNames)) + if (TryCreateCompatibleGeneratedMaterializer( + type, + profileType, + columnNames, + requireExplicitMapping: true, + out entry)) { - foreach (var candidate in _generatedMaterializers - .Where(pair => pair.Key.Type == type && pair.Key.ProfileType == profileType) - .OrderBy(pair => FormatColumnShape(pair.Key.ColumnNames), StringComparer.Ordinal)) - { - if (candidate.Key.ColumnNames.Count != columnNames.Length || - HasDuplicateColumnNames(candidate.Key.ColumnNames) || - !HaveSameColumnSet(candidate.Key.ColumnNames, columnNames) || - !GeneratedMaterializerMatchesExplicitMapping(type, profileType, candidate.Value.Columns)) - { - continue; - } - - var ordinalMap = CreateOrdinalMap(candidate.Key.ColumnNames, columnNames); - var candidateMaterializer = candidate.Value.Materialize; - materializer = record => candidateMaterializer(new OrdinalMappedDataRecord(record, ordinalMap)); - return true; - } + materializer = entry.Materialize; + return true; } materializer = null; @@ -432,10 +439,11 @@ private bool GeneratedMaterializerMatchesExplicitMapping( return true; } - private bool TryCreateReorderedGeneratedMaterializer( + private bool TryCreateCompatibleGeneratedMaterializer( Type type, Type profileType, string[] columnNames, + bool requireExplicitMapping, out GeneratedMaterializerEntry reorderedEntry) { reorderedEntry = null; @@ -447,13 +455,22 @@ private bool TryCreateReorderedGeneratedMaterializer( foreach (var candidate in _generatedMaterializers .Where(pair => pair.Key.Type == type && pair.Key.ProfileType == profileType) - .OrderBy(pair => FormatColumnShape(pair.Key.ColumnNames), StringComparer.Ordinal)) + .OrderByDescending(pair => pair.Key.ColumnNames.Count) + .ThenBy(pair => FormatColumnShape(pair.Key.ColumnNames), StringComparer.Ordinal)) { var generatedColumnNames = candidate.Key.ColumnNames; - if (generatedColumnNames.Count != columnNames.Length || + if (generatedColumnNames.Count > columnNames.Length || HasDuplicateColumnNames(generatedColumnNames) || - !HaveSameColumnSet(generatedColumnNames, columnNames) || - !GeneratedMaterializerMatchesEffectiveMapping(type, profileType, candidate.Value.Columns)) + !ContainsGeneratedColumnSet(generatedColumnNames, columnNames) || + !AdditionalColumnsAreUnmapped( + type, + profileType, + generatedColumnNames, + columnNames, + includeDapperDefaultMappings: !requireExplicitMapping) || + !(requireExplicitMapping + ? GeneratedMaterializerMatchesExplicitMapping(type, profileType, candidate.Value.Columns) + : GeneratedMaterializerMatchesEffectiveMapping(type, profileType, candidate.Value.Columns))) { continue; } @@ -497,14 +514,68 @@ internal string DescribeMissingGeneratedMaterializer(Type type, Type profileType return $"Entity '{type.FullName}' ({profileContext}) has no registered generated materializer. Requested result columns: {requestedShape}."; } + var duplicateColumns = columnNames + .GroupBy(columnName => columnName, StringComparer.Ordinal) + .Where(group => group.Count() > 1) + .Select(group => group.Key) + .OrderBy(columnName => columnName, StringComparer.Ordinal) + .ToList(); + if (duplicateColumns.Count > 0) + { + return $"Entity '{type.FullName}' ({profileContext}) requested result columns {requestedShape} contain duplicate/ambiguous mapped column names: {FormatColumnShape(duplicateColumns)}."; + } + var cacheKey = new MaterializationPlanCacheKey(type, profileType, columnNames); GeneratedMaterializerEntry exactEntry; if (_generatedMaterializers.TryGetValue(cacheKey, out exactEntry) && - !GeneratedMaterializerMatchesEffectiveMapping(type, profileType, exactEntry.Columns)) + !GeneratedMaterializerMatchesExplicitMapping(type, profileType, exactEntry.Columns)) { return $"Entity '{type.FullName}' ({profileContext}) has a generated materializer for requested result columns {requestedShape}, but its member/converter contract does not match the effective FluentMap mapping."; } + foreach (var candidate in candidates) + { + var missingColumns = candidate.Key.ColumnNames + .Where(columnName => !columnNames.Contains(columnName, StringComparer.Ordinal)) + .ToList(); + if (missingColumns.Count > 0) + { + continue; + } + + var unsafeAdditionalColumns = GetUnsafeAdditionalColumns( + type, + profileType, + candidate.Key.ColumnNames, + columnNames, + includeDapperDefaultMappings: false); + if (unsafeAdditionalColumns.Count > 0) + { + return $"Entity '{type.FullName}' ({profileContext}) requested result columns {requestedShape} contain additional columns that resolve to mapped members and cannot be ignored safely: {FormatColumnShape(unsafeAdditionalColumns)}."; + } + + if (!GeneratedMaterializerMatchesExplicitMapping(type, profileType, candidate.Value.Columns)) + { + return $"Entity '{type.FullName}' ({profileContext}) has a generated materializer compatible with requested result columns {requestedShape}, but its member/converter contract does not match the effective FluentMap mapping."; + } + } + + var closestCandidate = candidates + .Select(candidate => new + { + Shape = candidate.Key.ColumnNames, + Missing = candidate.Key.ColumnNames + .Where(columnName => !columnNames.Contains(columnName, StringComparer.Ordinal)) + .ToList() + }) + .OrderBy(candidate => candidate.Missing.Count) + .ThenBy(candidate => FormatColumnShape(candidate.Shape), StringComparer.Ordinal) + .First(); + if (closestCandidate.Missing.Count > 0) + { + return $"Entity '{type.FullName}' ({profileContext}) requested result columns {requestedShape} are missing required generated columns {FormatColumnShape(closestCandidate.Missing)}. Closest registered generated shape: {FormatColumnShape(closestCandidate.Shape)}."; + } + var availableShapes = string.Join(", ", candidates.Select(pair => FormatColumnShape(pair.Key.ColumnNames))); return $"Entity '{type.FullName}' ({profileContext}) has generated materializers, but none match requested result columns {requestedShape}. Registered generated shapes: {availableShapes}."; } @@ -800,11 +871,54 @@ private static bool HasDuplicateColumnNames(IEnumerable columnNames) return false; } - private static bool HaveSameColumnSet(IReadOnlyList generatedColumnNames, string[] requestedColumnNames) + private static bool ContainsGeneratedColumnSet(IReadOnlyList generatedColumnNames, string[] requestedColumnNames) { var requested = new HashSet(requestedColumnNames, StringComparer.Ordinal); - return requested.Count == generatedColumnNames.Count && - generatedColumnNames.All(columnName => requested.Contains(columnName)); + return generatedColumnNames.All(columnName => requested.Contains(columnName)); + } + + private bool AdditionalColumnsAreUnmapped( + Type type, + Type profileType, + IReadOnlyList generatedColumnNames, + IEnumerable requestedColumnNames, + bool includeDapperDefaultMappings) + { + return GetUnsafeAdditionalColumns( + type, + profileType, + generatedColumnNames, + requestedColumnNames, + includeDapperDefaultMappings).Count == 0; + } + + private List GetUnsafeAdditionalColumns( + Type type, + Type profileType, + IReadOnlyList generatedColumnNames, + IEnumerable requestedColumnNames, + bool includeDapperDefaultMappings) + { + var generated = new HashSet(generatedColumnNames, StringComparer.Ordinal); + var explicitPropertyMaps = GetExplicitPropertyMaps(type, profileType); + var defaultTypeMap = includeDapperDefaultMappings ? new DefaultTypeMap(type) : null; + var unsafeColumns = new List(); + + foreach (var columnName in requestedColumnNames.Where(columnName => !generated.Contains(columnName))) + { + var fluentMap = GetProfilePropertyMap(type, profileType, columnName); + var matchesMappedMemberName = explicitPropertyMaps.Any(map => + !map.Ignored && + string.Equals(map.PropertyInfo.Name, columnName, StringComparison.OrdinalIgnoreCase)); + if ((fluentMap != null && !fluentMap.Ignored) || + matchesMappedMemberName || + (fluentMap == null && defaultTypeMap != null && defaultTypeMap.GetMember(columnName) != null)) + { + unsafeColumns.Add(columnName); + } + } + + return unsafeColumns; } private static int[] CreateOrdinalMap(IReadOnlyList generatedColumnNames, string[] requestedColumnNames) diff --git a/src/Dapper.FluentMap/Materialization/MappedRowMaterializer.cs b/src/Dapper.FluentMap/Materialization/MappedRowMaterializer.cs index 4e5edc2..d8670b5 100644 --- a/src/Dapper.FluentMap/Materialization/MappedRowMaterializer.cs +++ b/src/Dapper.FluentMap/Materialization/MappedRowMaterializer.cs @@ -97,7 +97,7 @@ private static void ThrowMissingGeneratedMaterializer( throw new FluentMapConfigurationException( "Strict generated materialization is enabled, but FluentMap could not resolve a compatible generated materializer. " + runtime.Registry.DescribeMissingGeneratedMaterializer(typeof(TEntity), profileType, columnNames) + - " Register a generated materializer for this exact entity/profile/result-column shape, query the documented generated shape, or disable strict generated materialization for this runtime."); + " Register a compatible generated materializer, query a documented generated shape, or use QueryMapped* with strict generated materialization disabled for the non-strict runtime fallback."); } private static string[] GetColumnNames(IDataRecord reader) diff --git a/src/Dapper.FluentMap/QueryMappedExtensions.cs b/src/Dapper.FluentMap/QueryMappedExtensions.cs index c882e4e..c0b5e6e 100644 --- a/src/Dapper.FluentMap/QueryMappedExtensions.cs +++ b/src/Dapper.FluentMap/QueryMappedExtensions.cs @@ -877,6 +877,7 @@ internal static IEnumerable ExecuteMapped< internal static IEnumerable ExecuteGeneratedMapped( IDbConnection connection, string sql, + GeneratedParameters parameters, IDbTransaction transaction, int? commandTimeout, CommandType? commandType, @@ -916,6 +917,8 @@ internal static IEnumerable ExecuteGeneratedMapped( command.CommandType = commandType.Value; } + parameters?.AddParameters(command); + using (var reader = command.ExecuteReader()) { var results = new List(); diff --git a/test/Dapper.FluentMap.AotSmoke/Program.cs b/test/Dapper.FluentMap.AotSmoke/Program.cs index e651205..1fb19e6 100644 --- a/test/Dapper.FluentMap.AotSmoke/Program.cs +++ b/test/Dapper.FluentMap.AotSmoke/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Data; using System.Diagnostics.CodeAnalysis; using System.Linq; #if !AOT_SMOKE_GENERATED && !AOT_SMOKE_DI_GENERATED && !AOT_SMOKE_DI_EXPLICIT @@ -202,6 +203,12 @@ static void AssertStrictGeneratedQueryMappedMaterializer(FluentMapRuntime runtim using var connection = new SqliteConnection("Data Source=:memory:"); connection.Open(); + if (!runtime.Configuration.GeneratedMaterializers.Any(materializer => + materializer.EntityType == typeof(ReorderedCustomer))) + { + throw new InvalidOperationException("Generated startup registration did not include the strict smoke entity."); + } + var customer = runtime.QueryGeneratedMappedSingle( connection, "SELECT 42 AS customer_id;"); @@ -210,6 +217,34 @@ static void AssertStrictGeneratedQueryMappedMaterializer(FluentMapRuntime runtim throw new InvalidOperationException("Generated flat QueryMapped materializer was not used correctly."); } + var parameters = new GeneratedParameters() + .Add("Id", 43, DbType.Int32) + .Add("Name", "Parameterized", DbType.String, size: 64); + var parameterized = runtime.QueryGeneratedMappedSingle( + connection, + "SELECT @Id AS customer_id, @Name AS customer_name;", + parameters); + if (parameterized.Id != 43 || parameterized.Name != "Parameterized") + { + throw new InvalidOperationException("Parameterized strict generated query was not materialized correctly."); + } + + var reordered = runtime.QueryGeneratedMappedSingle( + connection, + "SELECT 'Reordered' AS customer_name, 44 AS customer_id;"); + if (reordered.Id != 44 || reordered.Name != "Reordered") + { + throw new InvalidOperationException("Reordered strict generated shape was not materialized correctly."); + } + + var additional = runtime.QueryGeneratedMappedSingle( + connection, + "SELECT 'trace' AS trace_id, 45 AS customer_id, 'Additional' AS customer_name;"); + if (additional.Id != 45 || additional.Name != "Additional") + { + throw new InvalidOperationException("Safe additional-column strict generated shape was not materialized correctly."); + } + var valueObjectCustomer = runtime.QueryGeneratedMappedSingle( connection, "SELECT '12345678909' AS cpf;"); @@ -258,6 +293,22 @@ public CustomerMap() } } +public sealed class ReorderedCustomer +{ + public int Id { get; set; } + + public string Name { get; set; } = string.Empty; +} + +public sealed class ReorderedCustomerMap : EntityMap +{ + public ReorderedCustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Name).ToColumn("customer_name"); + } +} + public sealed class LegacyProfile : IMappingProfile { } diff --git a/test/Dapper.FluentMap.GeneratedRegistration.Tests/GeneratedRegistrationIntegrationTests.cs b/test/Dapper.FluentMap.GeneratedRegistration.Tests/GeneratedRegistrationIntegrationTests.cs index ad2c1ac..1183bcb 100644 --- a/test/Dapper.FluentMap.GeneratedRegistration.Tests/GeneratedRegistrationIntegrationTests.cs +++ b/test/Dapper.FluentMap.GeneratedRegistration.Tests/GeneratedRegistrationIntegrationTests.cs @@ -133,13 +133,33 @@ public void GeneratedRegistrationShouldUseGeneratedMaterializersForReorderedFlat "SELECT 'Reordered' AS note, 44 AS age;"); var converted = connection.QueryMappedSingle( "SELECT '42' AS optional_score, 'A' AS status, 45 AS customer_id;"); + var strictExtra = FluentMapper.Runtime.QueryGeneratedMappedSingle( + connection, + "SELECT 'trace' AS trace_id, 'Additional' AS note, 46 AS age;"); + var strictParameterized = FluentMapper.Runtime.QueryGeneratedMappedSingle( + connection, + "SELECT @Age AS age, @Note AS note;", + new GeneratedParameters() + .Add("Age", 47, System.Data.DbType.Int32) + .Add("Note", "Parameterized", System.Data.DbType.String, size: 64)); Assert.Equal(44, nullable.Age); Assert.Equal("Reordered", nullable.Note); Assert.Equal(45, converted.Id); Assert.Equal(GeneratedAccountStatus.Active, converted.Status); Assert.Equal(42, converted.OptionalScore); + Assert.Equal(46, strictExtra.Age); + Assert.Equal("Additional", strictExtra.Note); + Assert.Equal(47, strictParameterized.Age); + Assert.Equal("Parameterized", strictParameterized.Note); Assert.Equal(0, FluentMapper.Registry.MaterializationPlanCacheEntryCount); + + var incompatibleValue = Assert.Throws(() => + FluentMapper.Runtime.QueryGeneratedMappedSingle( + connection, + "SELECT 'not-an-integer' AS age, 'Invalid' AS note;")); + Assert.Contains(typeof(GeneratedNullableCustomer).FullName, incompatibleValue.Message); + Assert.Contains(nameof(GeneratedNullableCustomer.Age), incompatibleValue.Message); } } finally diff --git a/test/Dapper.FluentMap.Generators.Tests/MappingRegistrationGeneratorTests.cs b/test/Dapper.FluentMap.Generators.Tests/MappingRegistrationGeneratorTests.cs index 4e51c71..988ba99 100644 --- a/test/Dapper.FluentMap.Generators.Tests/MappingRegistrationGeneratorTests.cs +++ b/test/Dapper.FluentMap.Generators.Tests/MappingRegistrationGeneratorTests.cs @@ -68,7 +68,8 @@ public void Configure() Assert.Contains(".AddMap()", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains(".AddGeneratedMaterializer(", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains("GeneratedMaterializerColumn.Map(\"customer_id\", \"Id\")", result.GeneratedSource, StringComparison.Ordinal); - Assert.Contains("entity.Id = Read(record, 0);", result.GeneratedSource, StringComparison.Ordinal); + Assert.Contains("entity.Id = Read(record, 0,", result.GeneratedSource, StringComparison.Ordinal); + Assert.Contains("Generated materialization failed for entity", result.GeneratedSource, StringComparison.Ordinal); } [Fact] @@ -98,7 +99,7 @@ public CustomerMap() Assert.Empty(result.DfmDiagnostics); Assert.Contains("GeneratedMaterializerColumn.Map(\"customer_id\", \"Id\")", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains("GeneratedMaterializerColumn.Map(\"full_name\", \"FullName\")", result.GeneratedSource, StringComparison.Ordinal); - Assert.Contains("entity.FullName = Read(record, 1);", result.GeneratedSource, StringComparison.Ordinal); + Assert.Contains("entity.FullName = Read(record, 1,", result.GeneratedSource, StringComparison.Ordinal); } [Fact] @@ -132,8 +133,8 @@ public CustomerMap() var result = RunGenerator(source); Assert.Empty(result.DfmDiagnostics); - Assert.Contains("var id = Read(record, 0);", result.GeneratedSource, StringComparison.Ordinal); - Assert.Contains("var fullName = Read(record, 1);", result.GeneratedSource, StringComparison.Ordinal); + Assert.Contains("var id = Read(record, 0,", result.GeneratedSource, StringComparison.Ordinal); + Assert.Contains("var fullName = Read(record, 1,", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains("entity = new global::Customer(id, fullName);", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains("FluentMapConfigurationException", result.GeneratedSource, StringComparison.Ordinal); } @@ -163,7 +164,7 @@ public CustomerMap() var result = RunGenerator(source); Assert.Empty(result.DfmDiagnostics); - Assert.Contains("entity.Age = Read(record, 0);", result.GeneratedSource, StringComparison.Ordinal); + Assert.Contains("entity.Age = Read(record, 0,", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains("return default(T);", result.GeneratedSource, StringComparison.Ordinal); } @@ -194,7 +195,7 @@ public CustomerMap() Assert.Empty(result.DfmDiagnostics); Assert.Contains("GeneratedMaterializerColumn.Map(\"customer_id\", \"Id\")", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains("GeneratedMaterializerColumn.Ignore(\"secret\")", result.GeneratedSource, StringComparison.Ordinal); - Assert.Contains("entity.Id = Read(record, 0);", result.GeneratedSource, StringComparison.Ordinal); + Assert.Contains("entity.Id = Read(record, 0,", result.GeneratedSource, StringComparison.Ordinal); Assert.DoesNotContain("entity.Secret =", result.GeneratedSource, StringComparison.Ordinal); } @@ -245,8 +246,8 @@ public CustomerMap() Assert.Contains("GeneratedMaterializerColumn.Map(\"insert_excluded\", \"InsertExcluded\")", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains("GeneratedMaterializerColumn.Map(\"update_excluded\", \"UpdateExcluded\")", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains("GeneratedMaterializerColumn.Ignore(\"secret\")", result.GeneratedSource, StringComparison.Ordinal); - Assert.Contains("entity.ReadOnlyName = Read(record, 1);", result.GeneratedSource, StringComparison.Ordinal); - Assert.Contains("entity.ComputedTotal = Read(record, 2);", result.GeneratedSource, StringComparison.Ordinal); + Assert.Contains("entity.ReadOnlyName = Read(record, 1,", result.GeneratedSource, StringComparison.Ordinal); + Assert.Contains("entity.ComputedTotal = Read(record, 2,", result.GeneratedSource, StringComparison.Ordinal); Assert.DoesNotContain("entity.Secret =", result.GeneratedSource, StringComparison.Ordinal); } @@ -596,7 +597,7 @@ public CustomerMap() Assert.Contains(".AddGeneratedMaterializer(", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains("GeneratedMaterializerColumn.Map(\"city\", \"Address.City\")", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains("if (!record.IsDBNull(0))", result.GeneratedSource, StringComparison.Ordinal); - Assert.Contains("node1.City = Read(record, 0);", result.GeneratedSource, StringComparison.Ordinal); + Assert.Contains("node1.City = Read(record, 0,", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains("entity.Address = null;", result.GeneratedSource, StringComparison.Ordinal); } @@ -684,8 +685,8 @@ public CustomerMap() Assert.Empty(result.DfmDiagnostics); Assert.Contains("GeneratedMaterializerColumn.Map(\"rank_level\", \"Rank.Level\")", result.GeneratedSource, StringComparison.Ordinal); Assert.Contains("GeneratedMaterializerColumn.Map(\"seniority_level\", \"Seniority.Level\")", result.GeneratedSource, StringComparison.Ordinal); - Assert.Contains("node1.Level = Read(record, 0);", result.GeneratedSource, StringComparison.Ordinal); - Assert.Contains("node2.Level = Read(record, 1);", result.GeneratedSource, StringComparison.Ordinal); + Assert.Contains("node1.Level = Read(record, 0,", result.GeneratedSource, StringComparison.Ordinal); + Assert.Contains("node2.Level = Read(record, 1,", result.GeneratedSource, StringComparison.Ordinal); } [Fact] diff --git a/test/Dapper.FluentMap.ProviderCompatibility.Tests/ProviderCompatibilityTests.cs b/test/Dapper.FluentMap.ProviderCompatibility.Tests/ProviderCompatibilityTests.cs index a5f4f18..21068af 100644 --- a/test/Dapper.FluentMap.ProviderCompatibility.Tests/ProviderCompatibilityTests.cs +++ b/test/Dapper.FluentMap.ProviderCompatibility.Tests/ProviderCompatibilityTests.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Dapper; +using Dapper.FluentMap.Configuration; using Dapper.FluentMap.Dommel; using Dapper.FluentMap.Dommel.Mapping; using Dapper.FluentMap.Mapping; @@ -265,6 +266,46 @@ record => } } + [Theory] + [MemberData(nameof(Providers))] + [Trait("Category", "ProviderCompatibility")] + public void StrictGeneratedParameterizedQueryShouldSupportSafeAdditionalProviderColumn(string providerName) + { + var provider = ProviderCase.Create(providerName); + provider.SkipIfUnavailable(); + + var runtime = new FluentMapConfigurationBuilder() + .AddMap(new MultipleCustomerMap()) + .AddGeneratedMaterializer( + new[] + { + GeneratedMaterializerColumn.Map("customer_id", nameof(MultipleCustomer.Id)), + GeneratedMaterializerColumn.Map("customer_name", nameof(MultipleCustomer.Name)) + }, + record => new MultipleCustomer + { + Id = Convert.ToInt32(record.GetValue(0)), + Name = Convert.ToString(record.GetValue(1)) + }) + .UseStrictGeneratedMaterialization() + .Build() + .CreateRuntime(); + var parameters = new GeneratedParameters() + .Add("Id", 22, DbType.Int32) + .Add("Name", "Strict Provider", DbType.String, size: 100); + + using (var connection = provider.OpenConnection()) + { + var customer = runtime.QueryGeneratedMappedSingle( + connection, + "SELECT @Id AS customer_id, @Name AS customer_name, 1 AS provider_trace;", + parameters); + + Assert.Equal(22, customer.Id); + Assert.Equal("Strict Provider", customer.Name); + } + } + [Theory] [MemberData(nameof(Providers))] [Trait("Category", "ProviderCompatibility")] diff --git a/test/Dapper.FluentMap.Tests/AdvancedQueryHardeningTests.cs b/test/Dapper.FluentMap.Tests/AdvancedQueryHardeningTests.cs index daa3f8e..047622e 100644 --- a/test/Dapper.FluentMap.Tests/AdvancedQueryHardeningTests.cs +++ b/test/Dapper.FluentMap.Tests/AdvancedQueryHardeningTests.cs @@ -165,7 +165,7 @@ public async Task QueryMappedUnbufferedAsyncShouldRemainStableAcrossParallelProf } [Fact] - public void QueryMultipleMappedShouldUseGeneratedAndRuntimeMaterializersOnIndependentParallelReaders() + public void QueryMultipleMappedShouldUseGeneratedMaterializersForSafeExtraColumnsOnIndependentParallelReaders() { PreTest(typeof(ConcurrentCustomer)); @@ -219,8 +219,8 @@ record => .ToList(); Assert.All(results, Assert.True); - Assert.Equal(30, generatedRows); - Assert.Equal(1, FluentMapper.Registry.MaterializationPlanCacheEntryCount); + Assert.Equal(60, generatedRows); + Assert.Equal(0, FluentMapper.Registry.MaterializationPlanCacheEntryCount); } finally { diff --git a/test/Dapper.FluentMap.Tests/GeneratedMaterializerContractTests.cs b/test/Dapper.FluentMap.Tests/GeneratedMaterializerContractTests.cs index 9b53908..38851ab 100644 --- a/test/Dapper.FluentMap.Tests/GeneratedMaterializerContractTests.cs +++ b/test/Dapper.FluentMap.Tests/GeneratedMaterializerContractTests.cs @@ -326,10 +326,103 @@ public void QueryGeneratedMappedShouldRejectDynamicParameterObjects() "SELECT @Id AS customer_id, 'GeneratedOnly' AS full_name;", new { Id = 86 })); - Assert.Contains("dynamic parameter objects", exception.Message, StringComparison.Ordinal); + Assert.Contains("unsupported", exception.Message, StringComparison.Ordinal); + Assert.Contains(typeof(GeneratedContractCustomer).FullName, exception.Message); + Assert.Contains(typeof(GeneratedParameters).FullName, exception.Message); + Assert.Contains("QueryMapped", exception.Message); + } + } + + [Fact] + [Trait("Category", "Integration")] + public void QueryGeneratedMappedShouldBindExplicitGeneratedParametersWithoutRuntimeFallback() + { + var runtime = CreateRuntime(builder => + { + builder + .AddMap(new GeneratedContractCustomerMap()) + .AddGeneratedMaterializer(DefaultColumns(), ReadDefaultGeneratedCustomer) + .UseStrictGeneratedMaterialization(); + }); + var parameters = new GeneratedParameters() + .Add("Id", 87, DbType.Int32) + .Add("Name", "Parameterized", DbType.String, size: 64); + + using (var connection = OpenConnection()) + { + var customer = runtime.QueryGeneratedMappedSingle( + connection, + "SELECT @Id AS customer_id, @Name AS full_name;", + parameters); + + Assert.Equal(87, customer.Id); + Assert.Equal("generated:Parameterized", customer.Name); + Assert.Equal(0, runtime.MaterializationPlanCacheEntryCount); + } + } + + [Fact] + [Trait("Category", "Integration")] + public void ProfiledQueryGeneratedMappedShouldBindExplicitGeneratedParameters() + { + var runtime = CreateRuntime(builder => + { + builder + .AddProfile() + .AddGeneratedMaterializer( + LegacyColumns(), + ReadLegacyGeneratedCustomer) + .UseStrictGeneratedMaterialization(); + }); + + using (var connection = OpenConnection()) + { + var customer = runtime.QueryGeneratedMappedSingle( + connection, + "SELECT @Id AS legacy_id, @Name AS legal_name;", + new GeneratedParameters() + .Add("Id", 88, DbType.Int32) + .Add("Name", "Profile", DbType.String, size: 64)); + + Assert.Equal(88, customer.Id); + Assert.Equal("legacy:Profile", customer.Name); + } + } + + [Fact] + public void QueryGeneratedMappedShouldRejectUnsupportedParametersBeforeOpeningConnection() + { + var runtime = CreateRuntime(builder => + { + builder + .AddMap(new GeneratedContractCustomerMap()) + .AddGeneratedMaterializer(DefaultColumns(), ReadDefaultGeneratedCustomer); + }); + + using (var connection = new SqliteConnection("Data Source=:memory:")) + { + Assert.Throws(() => + runtime.QueryGeneratedMappedSingle( + connection, + "not executable", + new { Id = 1 })); + + Assert.Equal(ConnectionState.Closed, connection.State); } } + [Fact] + public void GeneratedParametersShouldRejectEmptyAndEquivalentDuplicateNames() + { + var parameters = new GeneratedParameters().Add("@Id", 1, DbType.Int32); + + Assert.Throws(() => parameters.Add(" ", 2, DbType.Int32)); + Assert.Throws(() => parameters.Add("@", 2, DbType.Int32)); + Assert.Throws(() => parameters.Add("Other", 2, DbType.Int32, size: -1)); + var duplicate = Assert.Throws(() => parameters.Add("id", 2, DbType.Int32)); + Assert.Contains("already registered", duplicate.Message); + } + [Fact] [Trait("Category", "Integration")] public void StrictGeneratedRuntimeShouldRejectMissingGeneratedMaterializer() @@ -379,9 +472,9 @@ public void StrictGeneratedRuntimeShouldRejectUnsupportedColumnShape() connection, "SELECT 92 AS customer_id;")); - Assert.Contains("none match requested result columns", exception.Message); + Assert.Contains("missing required generated columns", exception.Message); Assert.Contains("['customer_id']", exception.Message); - Assert.Contains("['customer_id', 'full_name']", exception.Message); + Assert.Contains("['full_name']", exception.Message); Assert.DoesNotContain("SELECT", exception.Message); Assert.Equal(0, runtime.MaterializationPlanCacheEntryCount); } @@ -413,6 +506,82 @@ public void StrictGeneratedRuntimeShouldUseGeneratedMaterializerForReorderedColu } } + [Fact] + [Trait("Category", "Integration")] + public void GeneratedAndRuntimeMaterializationShouldBeEquivalentWithSafeAdditionalColumn() + { + var generatedRuntime = CreateRuntime(builder => + { + builder + .AddMap(new GeneratedContractCustomerMap()) + .AddGeneratedMaterializer(DefaultColumns(), ReadEquivalentGeneratedCustomer) + .UseStrictGeneratedMaterialization(); + }); + var runtime = CreateRuntime(builder => builder.AddMap(new GeneratedContractCustomerMap())); + const string sql = "SELECT 'trace' AS trace_id, 'Extra' AS full_name, 95 AS customer_id;"; + + using (var connection = OpenConnection()) + { + var generated = generatedRuntime.QueryGeneratedMappedSingle(connection, sql); + var reflected = runtime.QueryMappedSingle(connection, sql); + + Assert.Equal(reflected.Id, generated.Id); + Assert.Equal(reflected.Name, generated.Name); + Assert.Equal(0, generatedRuntime.MaterializationPlanCacheEntryCount); + Assert.Equal(1, runtime.MaterializationPlanCacheEntryCount); + } + } + + [Fact] + [Trait("Category", "Integration")] + public void StrictGeneratedRuntimeShouldRejectDuplicateMappedColumnNames() + { + var runtime = CreateRuntime(builder => + { + builder + .AddMap(new GeneratedContractCustomerMap()) + .AddGeneratedMaterializer(DefaultColumns(), ReadDefaultGeneratedCustomer) + .UseStrictGeneratedMaterialization(); + }); + + using (var connection = OpenConnection()) + { + var exception = Assert.Throws(() => + runtime.QueryGeneratedMappedSingle( + connection, + "SELECT 96 AS customer_id, 97 AS customer_id, 'Duplicate' AS full_name;")); + + Assert.Contains("duplicate/ambiguous", exception.Message); + Assert.Contains("customer_id", exception.Message); + Assert.Contains(typeof(GeneratedContractCustomer).FullName, exception.Message); + } + } + + [Fact] + [Trait("Category", "Integration")] + public void StrictGeneratedRuntimeShouldRejectAdditionalColumnThatMapsToEntityMember() + { + var runtime = CreateRuntime(builder => + { + builder + .AddMap(new GeneratedContractCustomerMap()) + .AddGeneratedMaterializer(DefaultColumns(), ReadDefaultGeneratedCustomer) + .UseStrictGeneratedMaterialization(); + }); + + using (var connection = OpenConnection()) + { + var exception = Assert.Throws(() => + runtime.QueryGeneratedMappedSingle( + connection, + "SELECT 98 AS customer_id, 'Mapped' AS full_name, 'Ambiguous' AS Name;")); + + Assert.Contains("additional columns", exception.Message); + Assert.Contains("['Name']", exception.Message); + Assert.Contains("cannot be ignored safely", exception.Message); + } + } + [Fact] public void StrictGeneratedLookupShouldChooseReorderedCandidateDeterministically() { @@ -458,6 +627,49 @@ public void StrictGeneratedLookupShouldChooseReorderedCandidateDeterministically } } + [Fact] + public void StrictGeneratedLookupShouldPreferMostSpecificCompatibleShape() + { + PreTest(typeof(GeneratedContractShape)); + + try + { + FluentMapper.Initialize(configuration => + { + configuration.AddMap(new GeneratedContractShapeMap()); + configuration.AddGeneratedMaterializer( + new[] + { + GeneratedMaterializerColumn.Map("alpha", nameof(GeneratedContractShape.First)), + GeneratedMaterializerColumn.Map("beta", nameof(GeneratedContractShape.Second)) + }, + record => new GeneratedContractShape { Source = "two-columns" }); + configuration.AddGeneratedMaterializer( + AlphabeticalShapeColumns(), + record => new GeneratedContractShape { Source = "three-columns" }); + }); + + var found = FluentMapper.Registry.TryGetStrictGeneratedMaterializer( + typeof(GeneratedContractShape), + profileType: null, + columnNames: new[] { "trace_id", "gamma", "alpha", "beta" }, + out var materializer); + + using (var reader = CreateReader( + new[] { "trace_id", "gamma", "alpha", "beta" }, + new object[] { "trace", 3, 1, 2 })) + { + Assert.True(found); + Assert.True(reader.Read()); + Assert.Equal("three-columns", Assert.IsType(materializer(reader)).Source); + } + } + finally + { + PreTest(typeof(GeneratedContractShape)); + } + } + [Fact] public void ResetShouldClearReorderedGeneratedMaterializerResolution() { @@ -822,6 +1034,15 @@ private static GeneratedContractCustomer ReadDefaultGeneratedCustomer(IDataRecor }; } + private static GeneratedContractCustomer ReadEquivalentGeneratedCustomer(IDataRecord record) + { + return new GeneratedContractCustomer + { + Id = Convert.ToInt32(record.GetValue(0)), + Name = Convert.ToString(record.GetValue(1)) + }; + } + private static GeneratedContractCustomer ReadLegacyGeneratedCustomer(IDataRecord record) { return new GeneratedContractCustomer From 81b7888bc72ab4c03a1f7f0a9d73d13f99a40fd7 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Wed, 30 Sep 2026 09:28:44 -0300 Subject: [PATCH 3/9] feat: complete advanced mapping capabilities (#214) --- CHANGELOG.md | 5 + COMPATIBILITY.md | 6 +- MIGRATION.md | 6 +- MIGRATION.pt-BR.md | 6 +- README.md | 8 +- README.pt-BR.md | 8 +- USAGE.md | 36 +- USAGE.pt-BR.md | 36 +- .../Dapper.FluentMap.Benchmarks/Program.cs | 70 +++ .../AnalyzerReleases.Unshipped.md | 1 + .../FluentMapConfigurationAnalyzer.cs | 71 ++- src/Dapper.FluentMap.Analyzers/README.md | 2 +- .../DommelWriteExtensions.cs | 232 +++++++++ .../Resolvers/DommelPersistenceMetadata.cs | 14 +- .../MappingRegistrationGenerator.cs | 25 + src/Dapper.FluentMap.Generators/README.md | 2 +- .../ImmutableFluentMapConfiguration.cs | 19 +- .../RuntimeConfigurationRegistryFactory.cs | 16 +- src/Dapper.FluentMap/FluentMapRuntime.cs | 178 +++++++ .../Mapping/EntityConstructionStrategy.cs | 58 +++ src/Dapper.FluentMap/Mapping/EntityMap.cs | 138 ++++- .../MappingConfigurationValidator.cs | 33 ++ src/Dapper.FluentMap/MappingRegistry.cs | 12 + .../Materialization/MappedRowMaterializer.cs | 38 +- .../NestedMaterializationPlan.cs | 171 +++++- .../Properties/AssemblyInfo.cs | 1 + src/Dapper.FluentMap/QueryMappedExtensions.cs | 492 +++++++++++++++++- .../FluentMapConfigurationAnalyzerTests.cs | 30 ++ .../DommelPersistenceIntegrationTests.cs | 268 ++++++++++ .../MappingRegistrationGeneratorTests.cs | 32 ++ .../ProviderCompatibilityTests.cs | 77 ++- .../ExplicitConstructionStrategyTests.cs | 204 ++++++++ .../QueryMappedMultiMappingTests.cs | 278 ++++++++++ 33 files changed, 2490 insertions(+), 83 deletions(-) create mode 100644 src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs create mode 100644 src/Dapper.FluentMap/Mapping/EntityConstructionStrategy.cs create mode 100644 test/Dapper.FluentMap.Tests/ExplicitConstructionStrategyTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0089526..63a1b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,15 @@ The historical archived package history is not reconstructed here. This changelo ### Added - Added reflection-free `GeneratedParameters` for parameterized strict-generated queries, including provider-matrix and Native AOT smoke coverage. +- Added Dommel `InsertMapped*`/`UpdateMapped*` operations that execute per-property write converters while preserving persistence exclusions and Dapper type-handler precedence. +- Added three-input FluentMap-controlled multi-mapping with multiple validated `splitOn` boundaries, per-segment profiles, async execution, null-child semantics and isolated-runtime APIs. +- Added explicit `ConstructUsing(...)` factory strategies for one to four mapped values, with configuration validation and deterministic generated/strict boundaries. +- Added analyzer diagnostic `DFM016` for duplicate explicit construction strategies. ### Changed - Generated materializers now accept safe additional unmapped reader columns and provide specific strict diagnostics for missing, duplicate/ambiguous and mapped additional columns. +- Two- and three-input multi-mapping now share a single segment materialization pipeline. ## [3.4.0] - 2026-09-27 diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index e33f394..ca739ee 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -107,7 +107,7 @@ var runtime = new FluentMapConfigurationBuilder() var customer = runtime.QueryMappedSingle(connection, sql); ``` -That isolation applies to `QueryMapped*`, `QueryGeneratedMapped*`, two-type `splitOn` multi-mapping including per-segment profile overloads, `ReadMapped*`, `QueryMultipleMapped`, streaming, profiles, converters, diagnostics and generated materializer lookup. It does not make normal Dapper queries or Dommel select a runtime per call. +That isolation applies to `QueryMapped*`, `QueryGeneratedMapped*`, two/three-type `splitOn` multi-mapping including per-segment profile overloads, explicit `ConstructUsing(...)` factories, `ReadMapped*`, `QueryMultipleMapped`, streaming, profiles, converters, diagnostics and generated materializer lookup. It does not make normal Dapper queries or Dommel select a runtime per call. ## API Compatibility @@ -133,5 +133,7 @@ The maintained 3.x line follows Semantic Versioning. See the GitHub releases pag - Full Native AOT support is not claimed. - Provider behavior outside the exact tested server/client versions listed above is not certified. - Dommel configuration isolation per `FluentMapRuntime` is not supported. -- Three-or-more-type multi-mapping, graph aggregation and CRUD generation are not implemented. +- Multi-mapping is supported for two or three input types. Four-or-more input types, automatic graph aggregation and generic core CRUD generation are not implemented. +- Explicit delegate construction strategies are supported by runtime materialization. The generator reports `DFM011` and strict generated mode rejects those shapes; no full Native AOT claim is made for delegate factories. +- Property write conversion is supported only by the Dommel adapter's opt-in `InsertMapped*` and `UpdateMapped*` APIs. Historical Dommel `Insert`/`Update` do not execute property converters. - `MappedGridReader` result sets must be consumed sequentially; concurrent reads are rejected deterministically. diff --git a/MIGRATION.md b/MIGRATION.md index fa5d0da..f41e776 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -298,7 +298,7 @@ For value objects stored through mapped components, use FluentMap-controlled mat Map(customer => customer.Cpf.Number).ToColumn("cpf"); ``` -The current materializer uses compatible public constructors. Factory methods are not used. +The runtime materializer still selects compatible public constructors by default. Maps that previously could not be materialized can now opt into `ConstructUsing(...)` with one to four explicitly mapped values. This factory path is supported by runtime materialization and isolated runtimes; generated materializers report `DFM011`, and strict generated mode rejects the unsupported shape without fallback. ## Profiles @@ -338,7 +338,9 @@ Map(product => product.Status) Normal `Dapper.Query()` does not execute property converters. Use Dapper `TypeHandler` for type-wide conversion. -Write converter metadata exists, but Dapper/Dommel writes do not execute it yet. +Configured write converters execute only through the Dommel adapter's opt-in `InsertMapped*` and `UpdateMapped*` methods. Historical Dommel `Insert`/`Update` behavior is unchanged. The property converter runs first; Dapper then applies any type handler registered for the converter output type. + +Three-input `QueryMapped` overloads now complement the historical two-input API. Supply two comma-separated unique `splitOn` boundaries. These APIs compose individual rows only and do not aggregate one-to-many graphs. ## Generated Registration diff --git a/MIGRATION.pt-BR.md b/MIGRATION.pt-BR.md index 616f93e..fb084d8 100644 --- a/MIGRATION.pt-BR.md +++ b/MIGRATION.pt-BR.md @@ -298,7 +298,7 @@ Para value objects armazenados por componentes mapeados, use materialização co Map(customer => customer.Cpf.Number).ToColumn("cpf"); ``` -O materializador atual usa construtores públicos compatíveis. Factory methods não são utilizados. +O materializador de runtime continua selecionando construtores públicos compatíveis por padrão. Maps que antes não podiam ser materializados agora podem optar por `ConstructUsing(...)`, com um a quatro valores explicitamente mapeados. Esse caminho de factory é suportado pela materialização em runtime e por runtimes isolados; materializadores gerados reportam `DFM011`, e o modo strict generated rejeita o shape sem fallback. ## Profiles @@ -338,7 +338,9 @@ Map(product => product.Status) `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. +Write converters configurados são executados apenas pelos métodos opt-in `InsertMapped*` e `UpdateMapped*` do adapter Dommel. O comportamento histórico de `Insert`/`Update` do Dommel permanece inalterado. O conversor da propriedade executa primeiro; depois, o Dapper aplica eventual type handler registrado para o tipo de saída do conversor. + +Overloads `QueryMapped` com três entradas agora complementam a API histórica de duas entradas. Informe duas fronteiras `splitOn` únicas, separadas por vírgula. Essas APIs apenas compõem linhas individuais e não agregam grafos um-para-muitos. ## Registro Gerado diff --git a/README.md b/README.md index b2581de..289bd73 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ See [MIGRATION.md](MIGRATION.md) when moving from FluentMap 2.x. | --- | --- | | Explicit property-to-column mapping | `EntityMap`, `Map(...).ToColumn(...)` | | Conventions and naming policies | `AddConvention(...)`, `UseNamingPolicy(...)` | -| Immutable constructor mapping | historical Dapper type-map bridge | +| Immutable/factory construction | constructor mapping, `ConstructUsing(...)` | | Nested objects and component value objects | `QueryMapped*` | | Alternate SQL shapes | mapping profiles | -| Two-type multi-mapping | `QueryMapped(...)` | +| Two/three-type multi-mapping | `QueryMapped<...>(..., splitOn: ...)` | | Multiple result sets | `QueryMultipleMapped*`, `ReadMapped*` | | Sync/async streaming | `QueryMappedUnbuffered*` | | Per-property conversion | property converters | @@ -41,7 +41,7 @@ See [MIGRATION.md](MIGRATION.md) when moving from FluentMap 2.x. | Strict generated path | `UseStrictGeneratedMaterialization()`, `QueryGeneratedMapped*` | | Isolated configuration | `FluentMapRuntime` | | Dependency Injection | `AddFluentMap(...)` | -| Dommel persistence metadata | `Dapper.FluentMap.Dommel` | +| Dommel persistence/write conversion | `InsertMapped*`, `UpdateMapped*` | | Compile-time diagnostics | `FluentMap.Analyzers` | Detailed examples are in [USAGE.md](USAGE.md). @@ -115,7 +115,7 @@ Examples include: - nested objects and component value objects; - profiles; -- two-type `splitOn` multi-mapping; +- two/three-type `splitOn` multi-mapping; - mapped multiple result sets; - sync/async streaming; - property converters; diff --git a/README.pt-BR.md b/README.pt-BR.md index 5043d81..733466a 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -30,10 +30,10 @@ Consulte [MIGRATION.pt-BR.md](MIGRATION.pt-BR.md) ao migrar do FluentMap 2.x. | --- | --- | | 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 | +| Construção imutável/factory | mapping de construtor, `ConstructUsing(...)` | | Objetos aninhados e value objects por componentes | `QueryMapped*` | | Formatos SQL alternativos | mapping profiles | -| Multi-mapping de dois tipos | `QueryMapped(...)` | +| Multi-mapping de dois/três tipos | `QueryMapped<...>(..., splitOn: ...)` | | Múltiplos result sets | `QueryMultipleMapped*`, `ReadMapped*` | | Streaming síncrono/assíncrono | `QueryMappedUnbuffered*` | | Conversão por propriedade | property converters | @@ -41,7 +41,7 @@ Consulte [MIGRATION.pt-BR.md](MIGRATION.pt-BR.md) ao migrar do FluentMap 2.x. | Caminho gerado estrito | `UseStrictGeneratedMaterialization()`, `QueryGeneratedMapped*` | | Configuração isolada | `FluentMapRuntime` | | Dependency Injection | `AddFluentMap(...)` | -| Metadata de persistência Dommel | `Dapper.FluentMap.Dommel` | +| Persistência/conversão de escrita Dommel | `InsertMapped*`, `UpdateMapped*` | | Diagnósticos em compilação | `FluentMap.Analyzers` | Exemplos detalhados estão em [USAGE.pt-BR.md](USAGE.pt-BR.md). @@ -115,7 +115,7 @@ Isso inclui: - objetos aninhados e value objects por componentes; - profiles; -- multi-mapping de dois tipos com `splitOn`; +- multi-mapping de dois/três tipos com `splitOn`; - múltiplos result sets mapeados; - streaming síncrono/assíncrono; - property converters; diff --git a/USAGE.md b/USAGE.md index d72d5d9..84d2ee1 100644 --- a/USAGE.md +++ b/USAGE.md @@ -145,7 +145,18 @@ var customer = connection.QueryMappedSingle( "SELECT 1 AS customer_id, '12345678909' AS cpf;"); ``` -The current materializer uses compatible public constructors. Factory methods are not used. +The runtime materializer uses compatible public constructors by default. A map can opt into an explicit factory when construction cannot follow that rule: + +```csharp +public CustomerMap() +{ + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Name).ToColumn("customer_name"); + ConstructUsing(customer => customer.Id, customer => customer.Name, Customer.Restore); +} +``` + +`ConstructUsing` supports one to four explicitly mapped values, validates every binding, and is preserved by isolated runtimes. Delegate factories are runtime-materialization strategies: the source generator registers the map but reports `DFM011` and does not emit a generated materializer. Strict generated mode therefore rejects that shape instead of silently using the factory through reflection. ## Mapping Profiles @@ -191,7 +202,7 @@ 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 +## Multi-Mapping For rows that contain two mapped entities, use explicit `splitOn` and a composition delegate: @@ -208,6 +219,17 @@ For common `LEFT JOIN` scenarios, if all columns in the second segment are `NULL This API performs row splitting; it does not aggregate repeated rows into one-to-many object graphs. +Three input segments are also supported through the same segment pipeline, including async and isolated-runtime variants: + +```csharp +var rows = connection.QueryMapped( + sql, + (customer, order, shipment) => new CustomerOrderShipment(customer, order, shipment), + splitOn: "order_id,shipment_id"); +``` + +Three-type calls require two comma-separated, unique boundaries in result order. Empty, missing, duplicated, ambiguous or out-of-order boundaries fail deterministically. Any all-`NULL` child segment is passed as `null`. The supported public arity is deliberately capped at three input types; compose rows manually for wider shapes. + ## Multiple Result Sets Use mapped multiple-result APIs when each result set needs FluentMap-controlled materialization: @@ -294,7 +316,15 @@ null/DBNull handling 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. +Dommel write conversion is opt-in through `InsertMapped*` and `UpdateMapped*` from `Dapper.FluentMap.Dommel`: + +```csharp +var id = connection.InsertMapped(product); +product.Id = Convert.ToInt32(id); +connection.UpdateMapped(product); +``` + +Only properties participating in the selected operation are converted. Converter output is then passed to Dapper, so a registered Dapper type handler for the converter output type runs after the property converter. Converter failures include entity, property, column and operation context and never retry with the original value. Historical Dommel `Insert`/`Update` remain unchanged because Dommel exposes no per-property parameter hook. ## Persistence Metadata And Dommel diff --git a/USAGE.pt-BR.md b/USAGE.pt-BR.md index d1ecd18..1485f83 100644 --- a/USAGE.pt-BR.md +++ b/USAGE.pt-BR.md @@ -145,7 +145,18 @@ 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. +Por padrão, o materializador de runtime usa construtores públicos compatíveis. Um mapping pode optar por uma factory explícita quando essa regra não for suficiente: + +```csharp +public CustomerMap() +{ + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Name).ToColumn("customer_name"); + ConstructUsing(customer => customer.Id, customer => customer.Name, Customer.Restore); +} +``` + +`ConstructUsing` aceita de um a quatro valores explicitamente mapeados, valida cada binding e é preservado por runtimes isolados. Factories delegate são estratégias de materialização em runtime: o source generator registra o map, mas reporta `DFM011` e não emite materializador gerado. Portanto, o modo strict generated rejeita esse shape sem fallback silencioso. ## Mapping Profiles @@ -191,7 +202,7 @@ 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 +## Multi-Mapping Para linhas que contêm duas entidades mapeadas, use `splitOn` explícito e um delegate de composição: @@ -208,6 +219,17 @@ Em cenários comuns com `LEFT JOIN`, se todas as colunas do segundo segmento for Essa API faz split de uma linha; ela não agrega linhas repetidas em grafos um-para-muitos. +Três segmentos de entrada também são suportados pelo mesmo pipeline, inclusive nas variantes assíncronas e de runtime isolado: + +```csharp +var rows = connection.QueryMapped( + sql, + (customer, order, shipment) => new CustomerOrderShipment(customer, order, shipment), + splitOn: "order_id,shipment_id"); +``` + +Chamadas com três tipos exigem duas fronteiras únicas, separadas por vírgula e na ordem do resultado. Fronteiras vazias, ausentes, duplicadas, ambíguas ou fora de ordem falham deterministicamente. Cada segmento filho contendo apenas `NULL` é entregue como `null`. A aridade pública é deliberadamente limitada a três tipos de entrada; componha manualmente shapes mais largos. + ## Múltiplos Result Sets Use as APIs de múltiplos resultados mapeados quando cada result set precisar de materialização controlada pelo FluentMap: @@ -294,7 +316,15 @@ tratamento de null/DBNull `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. +A conversão de escrita do Dommel é opt-in por `InsertMapped*` e `UpdateMapped*`, do pacote `Dapper.FluentMap.Dommel`: + +```csharp +var id = connection.InsertMapped(product); +product.Id = Convert.ToInt32(id); +connection.UpdateMapped(product); +``` + +Somente propriedades participantes da operação escolhida são convertidas. A saída do conversor é entregue ao Dapper; portanto, um type handler registrado para o tipo de saída é executado depois do conversor da propriedade. Falhas incluem contexto de entidade, propriedade, coluna e operação, sem nova tentativa com o valor original. Os métodos históricos `Insert`/`Update` do Dommel permanecem inalterados porque o Dommel não oferece hook por parâmetro/propriedade. ## Metadata de Persistência e Dommel diff --git a/benchmarks/Dapper.FluentMap.Benchmarks/Program.cs b/benchmarks/Dapper.FluentMap.Benchmarks/Program.cs index 100ab80..acd0c94 100644 --- a/benchmarks/Dapper.FluentMap.Benchmarks/Program.cs +++ b/benchmarks/Dapper.FluentMap.Benchmarks/Program.cs @@ -85,6 +85,8 @@ public async Task GlobalSetup() QueryMappedRuntimeTypeHandler(); QueryMappedGeneratedPropertyConverter(); QueryMappedRuntimePropertyConverter(); + QueryMappedTwoSegments(); + QueryMappedThreeSegments(); } [GlobalCleanup] @@ -351,6 +353,26 @@ public int QueryMappedRuntimePropertyConverter() .Count(); } + [Benchmark] + public int QueryMappedTwoSegments() + { + return _connection.QueryMapped( + "SELECT Id AS customer_id, Name AS customer_name, Id AS order_id, Balance AS order_total FROM BenchmarkRows;", + (customer, order) => customer.Id + order.Id, + splitOn: "order_id") + .Count(); + } + + [Benchmark] + public int QueryMappedThreeSegments() + { + return _connection.QueryMapped( + "SELECT Id AS customer_id, Name AS customer_name, Id AS order_id, Balance AS order_total, Id AS shipment_id, City AS shipment_status FROM BenchmarkRows;", + (customer, order, shipment) => customer.Id + order.Id + shipment.Id, + splitOn: "order_id,shipment_id") + .Count(); + } + private static SqliteConnection OpenPopulatedConnection() { var connection = new SqliteConnection("Data Source=:memory:"); @@ -993,3 +1015,51 @@ public ColdValueObjectCustomerMap() Map(customer => customer.Balance.Currency).ToColumn("currency"); } } + +public sealed class BenchmarkJoinCustomer +{ + public int Id { get; set; } + + public string Name { get; set; } = string.Empty; +} + +public sealed class BenchmarkJoinCustomerMap : EntityMap +{ + public BenchmarkJoinCustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Name).ToColumn("customer_name"); + } +} + +public sealed class BenchmarkJoinOrder +{ + public int Id { get; set; } + + public decimal Total { get; set; } +} + +public sealed class BenchmarkJoinOrderMap : EntityMap +{ + public BenchmarkJoinOrderMap() + { + Map(order => order.Id).ToColumn("order_id"); + Map(order => order.Total).ToColumn("order_total"); + } +} + +public sealed class BenchmarkJoinShipment +{ + public int Id { get; set; } + + public string Status { get; set; } = string.Empty; +} + +public sealed class BenchmarkJoinShipmentMap : EntityMap +{ + public BenchmarkJoinShipmentMap() + { + Map(shipment => shipment.Id).ToColumn("shipment_id"); + Map(shipment => shipment.Status).ToColumn("shipment_status"); + } +} diff --git a/src/Dapper.FluentMap.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapper.FluentMap.Analyzers/AnalyzerReleases.Unshipped.md index 11dc932..4fa5368 100644 --- a/src/Dapper.FluentMap.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Dapper.FluentMap.Analyzers/AnalyzerReleases.Unshipped.md @@ -12,3 +12,4 @@ DFM010 | Dapper.FluentMap.Configuration | Error | Mapping profile is registered DFM013 | Dapper.FluentMap.Configuration | Error | Persistence mapping behavior is invalid. DFM014 | Dapper.FluentMap.Configuration | Error | Property converter is invalid. DFM015 | Dapper.FluentMap.Configuration | Error | Property converter is configured more than once. +DFM016 | Dapper.FluentMap.Configuration | Error | Explicit construction strategy is configured more than once. diff --git a/src/Dapper.FluentMap.Analyzers/FluentMapConfigurationAnalyzer.cs b/src/Dapper.FluentMap.Analyzers/FluentMapConfigurationAnalyzer.cs index 823898d..b5c82d9 100644 --- a/src/Dapper.FluentMap.Analyzers/FluentMapConfigurationAnalyzer.cs +++ b/src/Dapper.FluentMap.Analyzers/FluentMapConfigurationAnalyzer.cs @@ -23,6 +23,7 @@ public sealed class FluentMapConfigurationAnalyzer : DiagnosticAnalyzer public const string InvalidPersistenceBehaviorDiagnosticId = "DFM013"; public const string InvalidPropertyConverterDiagnosticId = "DFM014"; public const string DuplicatePropertyConverterDiagnosticId = "DFM015"; + public const string DuplicateConstructionStrategyDiagnosticId = "DFM016"; private const string Category = "Dapper.FluentMap.Configuration"; private const string MappingNamespace = "Dapper.FluentMap.Mapping"; @@ -122,6 +123,16 @@ public sealed class FluentMapConfigurationAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, description: "A single FluentMap property mapping can have at most one read converter and at most one write converter."); + private static readonly DiagnosticDescriptor DuplicateConstructionStrategyRule = new DiagnosticDescriptor( + DuplicateConstructionStrategyDiagnosticId, + "Explicit construction strategy is configured more than once", + "Entity map constructor configures more than one ConstructUsing(...) strategy", + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "An entity map can configure only one explicit construction strategy.", + customTags: WellKnownDiagnosticTags.CompilationEnd); + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( InvalidMapExpressionRule, @@ -133,7 +144,8 @@ public sealed class FluentMapConfigurationAnalyzer : DiagnosticAnalyzer DuplicateProfileRegistrationRule, InvalidPersistenceBehaviorRule, InvalidPropertyConverterRule, - DuplicatePropertyConverterRule); + DuplicatePropertyConverterRule, + DuplicateConstructionStrategyRule); public override void Initialize(AnalysisContext context) { @@ -144,9 +156,10 @@ public override void Initialize(AnalysisContext context) { var constructorMapInvocations = new ConcurrentBag(); var profileRegistrations = new ConcurrentBag(); + var constructionStrategies = new ConcurrentBag(); startContext.RegisterSyntaxNodeAction( - nodeContext => AnalyzeInvocation(nodeContext, constructorMapInvocations, profileRegistrations), + nodeContext => AnalyzeInvocation(nodeContext, constructorMapInvocations, profileRegistrations, constructionStrategies), SyntaxKind.InvocationExpression); startContext.RegisterCompilationEndAction( @@ -154,6 +167,7 @@ public override void Initialize(AnalysisContext context) { AnalyzeConstructorMapInvocations(endContext, constructorMapInvocations); AnalyzeProfileRegistrations(endContext, profileRegistrations); + AnalyzeConstructionStrategies(endContext, constructionStrategies); }); }); } @@ -161,7 +175,8 @@ public override void Initialize(AnalysisContext context) private static void AnalyzeInvocation( SyntaxNodeAnalysisContext context, ConcurrentBag constructorMapInvocations, - ConcurrentBag profileRegistrations) + ConcurrentBag profileRegistrations, + ConcurrentBag constructionStrategies) { var invocation = (InvocationExpressionSyntax)context.Node; var method = context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol as IMethodSymbol; @@ -183,6 +198,19 @@ private static void AnalyzeInvocation( return; } + if (IsConstructUsingInvocation(method)) + { + var constructor = context.ContainingSymbol as IMethodSymbol; + if (constructor?.MethodKind == MethodKind.Constructor) + { + constructionStrategies.Add(new ConstructionStrategyInvocation( + constructor, + GetInvocationNameLocation(invocation))); + } + + return; + } + if (IsGenericAddMapInvocation(method)) { AnalyzeGenericAddMapInvocation(context, invocation, method); @@ -354,6 +382,23 @@ private static void AnalyzeConstructorMapInvocations( } } + private static void AnalyzeConstructionStrategies( + CompilationAnalysisContext context, + ConcurrentBag constructionStrategies) + { + foreach (var group in constructionStrategies.GroupBy( + invocation => invocation.Constructor, + SymbolEqualityComparer.Default)) + { + foreach (var duplicate in group.OrderBy(invocation => invocation.Location.SourceSpan.Start).Skip(1)) + { + context.ReportDiagnostic(Diagnostic.Create( + DuplicateConstructionStrategyRule, + duplicate.Location)); + } + } + } + private static void ReportDuplicateMemberPaths( CompilationAnalysisContext context, IList invocations) @@ -797,6 +842,13 @@ private static bool IsIncludeBaseInvocation(IMethodSymbol method) IsType(method.ContainingType.OriginalDefinition, MappingNamespace, "EntityMapBase`2"); } + private static bool IsConstructUsingInvocation(IMethodSymbol method) + { + return method.Name == "ConstructUsing" && + method.IsGenericMethod && + IsType(method.ContainingType.OriginalDefinition, MappingNamespace, "EntityMapBase`2"); + } + private static bool IsGenericAddMapInvocation(IMethodSymbol method) { return method.Name == "AddMap" && @@ -1404,6 +1456,19 @@ internal ProfileRegistrationInvocation( internal Location Location { get; } } + private sealed class ConstructionStrategyInvocation + { + internal ConstructionStrategyInvocation(IMethodSymbol constructor, Location location) + { + Constructor = constructor; + Location = location; + } + + internal IMethodSymbol Constructor { get; } + + internal Location Location { get; } + } + private enum PersistenceAction { None, diff --git a/src/Dapper.FluentMap.Analyzers/README.md b/src/Dapper.FluentMap.Analyzers/README.md index 34b9048..ea18dc8 100644 --- a/src/Dapper.FluentMap.Analyzers/README.md +++ b/src/Dapper.FluentMap.Analyzers/README.md @@ -2,7 +2,7 @@ Roslyn analyzers for statically provable `Dapper.FluentMap` configuration errors. -Install it alongside the core package when you want compile-time feedback for invalid map expressions, duplicate member paths, duplicate columns, invalid `IncludeBase()` usage, invalid generic map/profile registration, invalid type-based property converters and duplicate converter configuration in a fluent chain. +Install it alongside the core package when you want compile-time feedback for invalid map expressions, duplicate member paths, duplicate columns, invalid `IncludeBase()` usage, invalid generic map/profile registration, invalid type-based property converters, duplicate converter configuration in a fluent chain and duplicate `ConstructUsing(...)` strategies. ```bash dotnet add package FluentMap.Analyzers diff --git a/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs b/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs new file mode 100644 index 0000000..95047c8 --- /dev/null +++ b/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Dapper.FluentMap.Dommel.Resolvers; +using Dapper.FluentMap.Mapping; +using global::Dommel; + +namespace Dapper.FluentMap.Dommel +{ + /// + /// Provides Dommel-generated write operations that execute FluentMap property write converters. + /// + public static class DommelWriteExtensions + { + private static readonly ConcurrentDictionary> WriteConverterInvokers = + new ConcurrentDictionary>(); + + /// + /// Inserts an entity using Dommel mapping and persistence metadata, applying configured property write converters. + /// + /// The mapped entity type. + /// The database connection. + /// The entity to insert. + /// Optional transaction. + /// The scalar value returned by the provider-specific Dommel insert statement. + public static object InsertMapped( + this IDbConnection connection, + TEntity entity, + IDbTransaction transaction = null) + where TEntity : class + { + var command = CreateInsertCommand(connection, entity, transaction, CancellationToken.None); + return connection.ExecuteScalar(command); + } + + /// + /// Asynchronously inserts an entity using Dommel mapping and persistence metadata, applying configured property write converters. + /// + public static Task InsertMappedAsync( + this IDbConnection connection, + TEntity entity, + IDbTransaction transaction = null, + CancellationToken cancellationToken = default) + where TEntity : class + { + var command = CreateInsertCommand(connection, entity, transaction, cancellationToken); + return connection.ExecuteScalarAsync(command); + } + + /// + /// Updates an entity using Dommel mapping and persistence metadata, applying configured property write converters. + /// + /// when at least one row was updated. + public static bool UpdateMapped( + this IDbConnection connection, + TEntity entity, + IDbTransaction transaction = null) + where TEntity : class + { + var command = CreateUpdateCommand(connection, entity, transaction, CancellationToken.None); + return connection.Execute(command) > 0; + } + + /// + /// Asynchronously updates an entity using Dommel mapping and persistence metadata, applying configured property write converters. + /// + /// when at least one row was updated. + public static async Task UpdateMappedAsync( + this IDbConnection connection, + TEntity entity, + IDbTransaction transaction = null, + CancellationToken cancellationToken = default) + where TEntity : class + { + var command = CreateUpdateCommand(connection, entity, transaction, cancellationToken); + return await connection.ExecuteAsync(command).ConfigureAwait(false) > 0; + } + + private static CommandDefinition CreateInsertCommand( + IDbConnection connection, + TEntity entity, + IDbTransaction transaction, + CancellationToken cancellationToken) + where TEntity : class + { + EnsureArguments(connection, entity); + var type = typeof(TEntity); + var sqlBuilder = DommelMapper.GetSqlBuilder(connection); + var keyProperties = new DommelKeyPropertyResolver().ResolveKeyProperties(type); + var generatedKeys = new HashSet( + keyProperties.Where(key => key.IsGenerated).Select(key => key.Property)); + var properties = (DommelPersistenceMetadata.ResolveInsertProperties(type) ?? + new DommelPropertyResolver().ResolveProperties(type).Select(property => property.Property)) + .Where(property => !generatedKeys.Contains(property)) + .ToArray(); + var tableName = global::Dommel.Resolvers.Table(type, sqlBuilder); + var columnNames = properties + .Select(property => global::Dommel.Resolvers.Column(property, sqlBuilder, false)) + .ToArray(); + var parameterNames = properties.Select(property => sqlBuilder.PrefixParameter(property.Name)).ToArray(); + var sql = sqlBuilder.BuildInsert(type, tableName, columnNames, parameterNames); + var parameters = CreateParameters(type, entity, properties, "INSERT"); + DommelMapper.LogReceived?.Invoke(sql); + return new CommandDefinition(sql, parameters, transaction, cancellationToken: cancellationToken); + } + + private static CommandDefinition CreateUpdateCommand( + IDbConnection connection, + TEntity entity, + IDbTransaction transaction, + CancellationToken cancellationToken) + where TEntity : class + { + EnsureArguments(connection, entity); + var type = typeof(TEntity); + var sqlBuilder = DommelMapper.GetSqlBuilder(connection); + var keyProperties = new DommelKeyPropertyResolver().ResolveKeyProperties(type); + var keySet = new HashSet(keyProperties.Select(key => key.Property)); + var updateProperties = new DommelPropertyResolver() + .ResolveProperties(type) + .Where(property => !property.IsGenerated && !keySet.Contains(property.Property)) + .Select(property => property.Property) + .ToArray(); + var tableName = global::Dommel.Resolvers.Table(type, sqlBuilder); + var assignments = updateProperties.Select(property => + global::Dommel.Resolvers.Column(property, sqlBuilder, false) + " = " + + sqlBuilder.PrefixParameter(property.Name)); + var predicates = keyProperties.Select(key => + global::Dommel.Resolvers.Column(key.Property, sqlBuilder, false) + " = " + + sqlBuilder.PrefixParameter(key.Property.Name)); + var sql = "update " + tableName + " set " + string.Join(", ", assignments) + + " where " + string.Join(" and ", predicates); + var parameterProperties = updateProperties.Concat(keyProperties.Select(key => key.Property)).Distinct().ToArray(); + var parameters = CreateParameters(type, entity, parameterProperties, "UPDATE"); + DommelMapper.LogReceived?.Invoke(sql); + return new CommandDefinition(sql, parameters, transaction, cancellationToken: cancellationToken); + } + + private static DynamicParameters CreateParameters( + Type entityType, + TEntity entity, + IEnumerable properties, + string operation) + where TEntity : class + { + var parameters = new DynamicParameters(); + FluentMapper.EntityMaps.TryGetValue(entityType, out var entityMap); + + foreach (var property in properties) + { + var value = property.GetValue(entity, null); + var propertyMap = entityMap == null + ? null + : DommelPersistenceMetadata.ResolvePropertyMap(entityType, entityMap, property.Name); + var conversion = propertyMap == null + ? PropertyConversionMetadata.Default + : PropertyMapConversion.GetConversion(propertyMap); + + if (conversion.HasWriteConverter && value != null) + { + try + { + value = InvokeWriteConverter(conversion.WriteConverter, value); + } + catch (Exception exception) + { + throw new FluentMapConfigurationException( + $"Write converter failed during Dommel {operation} for entity '{entityType.FullName}', property '{property.Name}', column '{propertyMap.ColumnName}', converter '{conversion.WriteConverter.ConverterType.FullName}'. The original property value was not written.", + exception); + } + } + + parameters.Add(property.Name, value); + } + + return parameters; + } + + private static object InvokeWriteConverter(PropertyConverterMetadata converter, object value) + { + return WriteConverterInvokers.GetOrAdd(converter, CreateWriteConverterInvoker)(value); + } + + private static Func CreateWriteConverterInvoker(PropertyConverterMetadata converter) + { + var input = Expression.Parameter(typeof(object), "value"); + var converterValue = Expression.Constant(converter.Converter); + Expression call; + + if (converter.Converter is Delegate) + { + call = Expression.Invoke( + Expression.Convert(converterValue, converter.Converter.GetType()), + Expression.Convert(input, converter.PropertyType)); + } + else + { + var interfaceType = typeof(IWritePropertyConverter<,>).MakeGenericType( + converter.PropertyType, + converter.DatabaseType); + call = Expression.Call( + Expression.Convert(converterValue, interfaceType), + interfaceType.GetMethod(nameof(IWritePropertyConverter.ConvertToDatabase)), + Expression.Convert(input, converter.PropertyType)); + } + + return Expression.Lambda>( + Expression.Convert(call, typeof(object)), + input).Compile(); + } + + private static void EnsureArguments(IDbConnection connection, TEntity entity) + where TEntity : class + { + if (connection == null) + { + throw new ArgumentNullException(nameof(connection)); + } + + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + } + } +} diff --git a/src/Dapper.FluentMap.Dommel/Resolvers/DommelPersistenceMetadata.cs b/src/Dapper.FluentMap.Dommel/Resolvers/DommelPersistenceMetadata.cs index 4982fbb..52b829b 100644 --- a/src/Dapper.FluentMap.Dommel/Resolvers/DommelPersistenceMetadata.cs +++ b/src/Dapper.FluentMap.Dommel/Resolvers/DommelPersistenceMetadata.cs @@ -45,9 +45,17 @@ internal static IEnumerable ResolveInsertProperties(Type type) .ResolveProperties(type) .Where(property => { - PropertyPersistenceMetadata persistence; - return !TryGetPersistence(property.Property, out persistence) || - persistence.ParticipatesInInsert; + var propertyMap = ResolvePropertyMap(type, entityMap, property.Property.Name); + if (propertyMap == null) + { + return true; + } + + var mapWithPersistence = propertyMap as IPropertyMapWithPersistenceMetadata; + var persistence = mapWithPersistence == null + ? (propertyMap.Ignored ? PropertyPersistenceMetadata.Ignored : PropertyPersistenceMetadata.Default) + : mapWithPersistence.Persistence; + return persistence.ParticipatesInInsert; }) .Select(property => property.Property); } diff --git a/src/Dapper.FluentMap.Generators/MappingRegistrationGenerator.cs b/src/Dapper.FluentMap.Generators/MappingRegistrationGenerator.cs index b4f0148..dfb59ac 100644 --- a/src/Dapper.FluentMap.Generators/MappingRegistrationGenerator.cs +++ b/src/Dapper.FluentMap.Generators/MappingRegistrationGenerator.cs @@ -962,6 +962,12 @@ private static GeneratedMaterializerInfo TryCreateGeneratedMaterializer( return null; } + if (ContainsExplicitConstructionInvocation(constructor, semanticModel, cancellationToken)) + { + skipReason = "explicit ConstructUsing(...) strategies are supported by runtime materialization but are not emitted by generated materializers"; + return null; + } + var mapInvocations = new List(); foreach (var invocation in constructor.Body.DescendantNodes().OfType()) { @@ -1124,6 +1130,25 @@ private static bool ContainsIncludeBaseInvocation( return false; } + private static bool ContainsExplicitConstructionInvocation( + ConstructorDeclarationSyntax constructor, + SemanticModel semanticModel, + System.Threading.CancellationToken cancellationToken) + { + foreach (var invocation in constructor.Body.DescendantNodes().OfType()) + { + var method = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol as IMethodSymbol; + if (method != null && + method.Name == "ConstructUsing" && + IsType(method.ContainingType.OriginalDefinition, MappingNamespace, "EntityMapBase`2")) + { + return true; + } + } + + return false; + } + private static bool TryCreateDirectMapInvocation( InvocationExpressionSyntax mapInvocation, INamedTypeSymbol mapType, diff --git a/src/Dapper.FluentMap.Generators/README.md b/src/Dapper.FluentMap.Generators/README.md index 35f8ff7..23ecbe8 100644 --- a/src/Dapper.FluentMap.Generators/README.md +++ b/src/Dapper.FluentMap.Generators/README.md @@ -19,4 +19,4 @@ FluentMapper.Initialize(config => }); ``` -Generated registration avoids reflection-based assembly scanning for maps in the current compilation. It does not scan referenced assemblies, execute map constructors during generation, parse SQL or replace `FluentMapper.Validate()`. Missing, additional or duplicate-column shapes continue to use the runtime fallback, or fail deterministically when strict generated materialization is enabled. +Generated registration avoids reflection-based assembly scanning for maps in the current compilation. It does not scan referenced assemblies, execute map constructors during generation, parse SQL or replace `FluentMapper.Validate()`. Explicit delegate `ConstructUsing(...)` factories remain runtime-only and produce `DFM011`; strict generated materialization rejects those shapes rather than using a hidden fallback. Missing, mapped-additional or duplicate-column shapes likewise fail deterministically in strict mode. diff --git a/src/Dapper.FluentMap/Configuration/ImmutableFluentMapConfiguration.cs b/src/Dapper.FluentMap/Configuration/ImmutableFluentMapConfiguration.cs index 76b2484..e08a806 100644 --- a/src/Dapper.FluentMap/Configuration/ImmutableFluentMapConfiguration.cs +++ b/src/Dapper.FluentMap/Configuration/ImmutableFluentMapConfiguration.cs @@ -123,12 +123,14 @@ private EntityMappingConfiguration( Type entityType, Type mapType, IReadOnlyList propertyMaps, - IReadOnlyList includedBaseTypes) + IReadOnlyList includedBaseTypes, + EntityConstructionStrategy constructionStrategy) { EntityType = entityType; MapType = mapType; PropertyMaps = propertyMaps; IncludedBaseTypes = includedBaseTypes; + ConstructionStrategy = constructionStrategy; } /// @@ -151,6 +153,8 @@ private EntityMappingConfiguration( /// public IReadOnlyList IncludedBaseTypes { get; } + internal EntityConstructionStrategy ConstructionStrategy { get; } + internal static EntityMappingConfiguration Create(Type entityType, IEntityMap map) { if (entityType == null) @@ -164,13 +168,15 @@ internal static EntityMappingConfiguration Create(Type entityType, IEntityMap ma } var includedBaseTypes = map as IEntityMapWithIncludedBaseTypes; + var mapWithConstruction = map as IEntityMapWithConstructionStrategy; return new EntityMappingConfiguration( entityType, map.GetType(), new ReadOnlyCollection( map.PropertyMaps.Select(PropertyMappingConfiguration.Create).ToList()), new ReadOnlyCollection( - includedBaseTypes == null ? new List() : includedBaseTypes.IncludedBaseTypes.ToList())); + includedBaseTypes == null ? new List() : includedBaseTypes.IncludedBaseTypes.ToList()), + mapWithConstruction?.ConstructionStrategy); } } @@ -184,13 +190,15 @@ private ProfileMappingConfiguration( Type profileType, Type mapType, IReadOnlyList propertyMaps, - IReadOnlyList includedBaseTypes) + IReadOnlyList includedBaseTypes, + EntityConstructionStrategy constructionStrategy) { EntityType = entityType; ProfileType = profileType; MapType = mapType; PropertyMaps = propertyMaps; IncludedBaseTypes = includedBaseTypes; + ConstructionStrategy = constructionStrategy; } /// @@ -218,6 +226,8 @@ private ProfileMappingConfiguration( /// public IReadOnlyList IncludedBaseTypes { get; } + internal EntityConstructionStrategy ConstructionStrategy { get; } + internal static ProfileMappingConfiguration Create(Type entityType, Type profileType, IEntityMap map) { var entityMap = EntityMappingConfiguration.Create(entityType, map); @@ -226,7 +236,8 @@ internal static ProfileMappingConfiguration Create(Type entityType, Type profile profileType, entityMap.MapType, entityMap.PropertyMaps, - entityMap.IncludedBaseTypes); + entityMap.IncludedBaseTypes, + entityMap.ConstructionStrategy); } } diff --git a/src/Dapper.FluentMap/Configuration/RuntimeConfigurationRegistryFactory.cs b/src/Dapper.FluentMap/Configuration/RuntimeConfigurationRegistryFactory.cs index 41fa502..0828157 100644 --- a/src/Dapper.FluentMap/Configuration/RuntimeConfigurationRegistryFactory.cs +++ b/src/Dapper.FluentMap/Configuration/RuntimeConfigurationRegistryFactory.cs @@ -54,13 +54,19 @@ internal static MappingRegistry Create(ImmutableFluentMapConfiguration configura private sealed class SnapshotEntityMap : IEntityMap, IEntityMapWithIncludedBaseTypes, + IEntityMapWithConstructionStrategy, IRuntimeEntityMapMetadata { - private SnapshotEntityMap(Type mapType, IList propertyMaps, IList includedBaseTypes) + private SnapshotEntityMap( + Type mapType, + IList propertyMaps, + IList includedBaseTypes, + EntityConstructionStrategy constructionStrategy) { MapType = mapType; PropertyMaps = propertyMaps; IncludedBaseTypes = includedBaseTypes; + ConstructionStrategy = constructionStrategy; } public IList PropertyMaps { get; } @@ -69,12 +75,15 @@ private SnapshotEntityMap(Type mapType, IList propertyMaps, IList< public Type MapType { get; } + public EntityConstructionStrategy ConstructionStrategy { get; } + internal static SnapshotEntityMap FromEntityConfiguration(EntityMappingConfiguration map) { return new SnapshotEntityMap( map.MapType, map.PropertyMaps.Select(SnapshotPropertyMap.FromConfiguration).Cast().ToList(), - map.IncludedBaseTypes.ToList()); + map.IncludedBaseTypes.ToList(), + map.ConstructionStrategy); } internal static SnapshotEntityMap FromProfileConfiguration(ProfileMappingConfiguration map) @@ -82,7 +91,8 @@ internal static SnapshotEntityMap FromProfileConfiguration(ProfileMappingConfigu return new SnapshotEntityMap( map.MapType, map.PropertyMaps.Select(SnapshotPropertyMap.FromConfiguration).Cast().ToList(), - map.IncludedBaseTypes.ToList()); + map.IncludedBaseTypes.ToList(), + map.ConstructionStrategy); } } diff --git a/src/Dapper.FluentMap/FluentMapRuntime.cs b/src/Dapper.FluentMap/FluentMapRuntime.cs index c792fba..de8248b 100644 --- a/src/Dapper.FluentMap/FluentMapRuntime.cs +++ b/src/Dapper.FluentMap/FluentMapRuntime.cs @@ -186,6 +186,184 @@ public IEnumerable QueryMapped< runtime: this); } + /// + /// Executes a query, splits each row into three segments using this runtime and composes the return value. + /// + /// + /// must contain two comma-separated, unique boundaries in result order. + /// All-database-null child segments are passed to as . + /// + [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + public IEnumerable QueryMapped< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn>( + IDbConnection connection, + string sql, + Func map, + object param = null, + IDbTransaction transaction = null, + int? commandTimeout = null, + CommandType? commandType = null, + string splitOn = null) + where TFirst : class + where TSecond : class + where TThird : class + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + return QueryMappedExtensions.ExecuteMapped( + connection, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType), + map, + splitOn, + null, + null, + null, + this); + } + + /// + /// Asynchronously executes a query, materializes three segments using this runtime and composes the return value. + /// + [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + public Task> QueryMappedAsync< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn>( + IDbConnection connection, + string sql, + Func map, + object param = null, + IDbTransaction transaction = null, + int? commandTimeout = null, + CommandType? commandType = null, + string splitOn = null, + CancellationToken cancellationToken = default) + where TFirst : class + where TSecond : class + where TThird : class + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + if (map == null) + { + throw new ArgumentNullException(nameof(map)); + } + + return QueryMappedExtensions.ExecuteMappedSegmentsAsync( + connection, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, cancellationToken), + new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, + new Type[] { null, null, null }, + values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + splitOn, + this); + } + + /// + /// Asynchronously executes a query and materializes three profiled segments using this runtime. + /// + [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + public Task> QueryMappedAsync< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn, + TFirstProfile, + TSecondProfile, + TThirdProfile>( + IDbConnection connection, + string sql, + Func map, + object param = null, + IDbTransaction transaction = null, + int? commandTimeout = null, + CommandType? commandType = null, + string splitOn = null, + CancellationToken cancellationToken = default) + where TFirst : class + where TSecond : class + where TThird : class + where TFirstProfile : IMappingProfile + where TSecondProfile : IMappingProfile + where TThirdProfile : IMappingProfile + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + if (map == null) + { + throw new ArgumentNullException(nameof(map)); + } + + return QueryMappedExtensions.ExecuteMappedSegmentsAsync( + connection, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, cancellationToken), + new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, + new[] { typeof(TFirstProfile), typeof(TSecondProfile), typeof(TThirdProfile) }, + values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + splitOn, + this); + } + + /// + /// Executes a query and materializes three profiled segments using this runtime. + /// + [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + public IEnumerable QueryMapped< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn, + TFirstProfile, + TSecondProfile, + TThirdProfile>( + IDbConnection connection, + string sql, + Func map, + object param = null, + IDbTransaction transaction = null, + int? commandTimeout = null, + CommandType? commandType = null, + string splitOn = null) + where TFirst : class + where TSecond : class + where TThird : class + where TFirstProfile : IMappingProfile + where TSecondProfile : IMappingProfile + where TThirdProfile : IMappingProfile + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + return QueryMappedExtensions.ExecuteMapped( + connection, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType), + map, + splitOn, + typeof(TFirstProfile), + typeof(TSecondProfile), + typeof(TThirdProfile), + this); + } + /// /// Executes a query, splits each row at , materializes two profiled segments using this runtime and composes the return value. /// diff --git a/src/Dapper.FluentMap/Mapping/EntityConstructionStrategy.cs b/src/Dapper.FluentMap/Mapping/EntityConstructionStrategy.cs new file mode 100644 index 0000000..784b8b6 --- /dev/null +++ b/src/Dapper.FluentMap/Mapping/EntityConstructionStrategy.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Dapper.FluentMap.Mapping +{ + internal interface IEntityMapWithConstructionStrategy + { + EntityConstructionStrategy ConstructionStrategy { get; } + } + + internal sealed class EntityConstructionStrategy + { + internal EntityConstructionStrategy( + Type entityType, + IEnumerable bindings, + Func factory) + { + EntityType = entityType ?? throw new ArgumentNullException(nameof(entityType)); + Bindings = (bindings ?? throw new ArgumentNullException(nameof(bindings))).ToArray(); + Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + + if (Bindings.Count == 0) + { + throw new FluentMapConfigurationException( + $"Explicit construction for entity '{entityType.FullName}' requires at least one mapped property binding."); + } + + var duplicate = Bindings + .GroupBy(binding => binding.MemberPath) + .FirstOrDefault(group => group.Count() > 1); + if (duplicate != null) + { + throw new FluentMapConfigurationException( + $"Explicit construction for entity '{entityType.FullName}' binds property path '{duplicate.Key}' more than once."); + } + } + + internal Type EntityType { get; } + + internal IReadOnlyList Bindings { get; } + + internal Func Factory { get; } + } + + internal sealed class ConstructionValueBinding + { + internal ConstructionValueBinding(MemberPath memberPath, Type valueType) + { + MemberPath = memberPath ?? throw new ArgumentNullException(nameof(memberPath)); + ValueType = valueType ?? throw new ArgumentNullException(nameof(valueType)); + } + + internal MemberPath MemberPath { get; } + + internal Type ValueType { get; } + } +} diff --git a/src/Dapper.FluentMap/Mapping/EntityMap.cs b/src/Dapper.FluentMap/Mapping/EntityMap.cs index a66251e..374fbbc 100644 --- a/src/Dapper.FluentMap/Mapping/EntityMap.cs +++ b/src/Dapper.FluentMap/Mapping/EntityMap.cs @@ -59,7 +59,7 @@ internal interface IEntityMapWithIncludedBaseTypes public abstract class EntityMapBase< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] TEntity, - TPropertyMap> : IEntityMap, IEntityMapWithIncludedBaseTypes + TPropertyMap> : IEntityMap, IEntityMapWithIncludedBaseTypes, IEntityMapWithConstructionStrategy where TPropertyMap : IPropertyMap { /// @@ -78,8 +78,12 @@ protected EntityMapBase() IList IEntityMapWithIncludedBaseTypes.IncludedBaseTypes => IncludedBaseTypes; + EntityConstructionStrategy IEntityMapWithConstructionStrategy.ConstructionStrategy => ConstructionStrategy; + private IList IncludedBaseTypes { get; } + private EntityConstructionStrategy ConstructionStrategy { get; set; } + /// /// Returns an instance of which can perform custom mapping /// for the specified property on . @@ -126,6 +130,119 @@ protected void IncludeBase() IncludedBaseTypes.Add(baseType); } + /// + /// Configures an explicit factory that constructs the entity from one explicitly mapped property value. + /// + /// The first mapped value type. + /// The mapped property path supplied as the first factory argument. + /// The factory invoked for each materialized row. + protected void ConstructUsing( + Expression> value1, + Func factory) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + SetConstructionStrategy( + new[] { CreateConstructionBinding(value1) }, + values => factory((TValue1)values[0])); + } + + /// + /// Configures an explicit factory that constructs the entity from two explicitly mapped property values. + /// + /// The first mapped value type. + /// The second mapped value type. + /// The mapped property path supplied as the first factory argument. + /// The mapped property path supplied as the second factory argument. + /// The factory invoked for each materialized row. + protected void ConstructUsing( + Expression> value1, + Expression> value2, + Func factory) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + SetConstructionStrategy( + new[] { CreateConstructionBinding(value1), CreateConstructionBinding(value2) }, + values => factory((TValue1)values[0], (TValue2)values[1])); + } + + /// + /// Configures an explicit factory that constructs the entity from three explicitly mapped property values. + /// + /// The first mapped value type. + /// The second mapped value type. + /// The third mapped value type. + /// The mapped property path supplied as the first factory argument. + /// The mapped property path supplied as the second factory argument. + /// The mapped property path supplied as the third factory argument. + /// The factory invoked for each materialized row. + protected void ConstructUsing( + Expression> value1, + Expression> value2, + Expression> value3, + Func factory) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + SetConstructionStrategy( + new[] + { + CreateConstructionBinding(value1), + CreateConstructionBinding(value2), + CreateConstructionBinding(value3) + }, + values => factory((TValue1)values[0], (TValue2)values[1], (TValue3)values[2])); + } + + /// + /// Configures an explicit factory that constructs the entity from four explicitly mapped property values. + /// + /// The first mapped value type. + /// The second mapped value type. + /// The third mapped value type. + /// The fourth mapped value type. + /// The mapped property path supplied as the first factory argument. + /// The mapped property path supplied as the second factory argument. + /// The mapped property path supplied as the third factory argument. + /// The mapped property path supplied as the fourth factory argument. + /// The factory invoked for each materialized row. + protected void ConstructUsing( + Expression> value1, + Expression> value2, + Expression> value3, + Expression> value4, + Func factory) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + SetConstructionStrategy( + new[] + { + CreateConstructionBinding(value1), + CreateConstructionBinding(value2), + CreateConstructionBinding(value3), + CreateConstructionBinding(value4) + }, + values => factory( + (TValue1)values[0], + (TValue2)values[1], + (TValue3)values[2], + (TValue4)values[3])); + } + /// /// When overridden in a derived class, gets the property mapping for the specified property. /// @@ -133,6 +250,25 @@ protected void IncludeBase() /// An instance of . protected abstract TPropertyMap GetPropertyMap(PropertyInfo info); + private static ConstructionValueBinding CreateConstructionBinding( + Expression> expression) + { + return new ConstructionValueBinding(ReflectionHelper.GetMemberPath(expression), typeof(TValue)); + } + + private void SetConstructionStrategy( + IEnumerable bindings, + Func factory) + { + if (ConstructionStrategy != null) + { + throw new FluentMapConfigurationException( + $"Entity '{typeof(TEntity).FullName}' already has an explicit construction strategy."); + } + + ConstructionStrategy = new EntityConstructionStrategy(typeof(TEntity), bindings, factory); + } + private void ThrowIfDuplicateMapping(IPropertyMap map) { var memberPath = PropertyMapIdentity.GetMemberPath(map); diff --git a/src/Dapper.FluentMap/MappingConfigurationValidator.cs b/src/Dapper.FluentMap/MappingConfigurationValidator.cs index a343680..ab49536 100644 --- a/src/Dapper.FluentMap/MappingConfigurationValidator.cs +++ b/src/Dapper.FluentMap/MappingConfigurationValidator.cs @@ -48,6 +48,39 @@ internal static void ValidateComposedEntityMap(Type entityType, IEntityMap entit var maps = GetEntityMapDescriptors(entityType, propertyMaps, entityMap.GetType(), "composed entity map").ToList(); ValidateColumnConflicts(entityType, maps, "composed entity map", entityMap.GetType()); ValidateNestedMaterializationPaths(entityType, maps, "composed entity map", entityMap.GetType()); + ValidateConstructionStrategy(entityType, entityMap, propertyMaps); + } + + private static void ValidateConstructionStrategy( + Type entityType, + IEntityMap entityMap, + IEnumerable propertyMaps) + { + var strategy = (entityMap as IEntityMapWithConstructionStrategy)?.ConstructionStrategy; + if (strategy == null) + { + return; + } + + if (strategy.EntityType != entityType) + { + throw new FluentMapConfigurationException( + $"Explicit construction strategy for '{FormatType(strategy.EntityType)}' cannot be used by entity map '{FormatType(entityMap.GetType())}' for '{FormatType(entityType)}'."); + } + + var mappedPaths = propertyMaps + .Where(map => !map.Ignored) + .Select(PropertyMapIdentity.GetMemberPath) + .ToList(); + + foreach (var binding in strategy.Bindings) + { + if (!mappedPaths.Contains(binding.MemberPath)) + { + throw new FluentMapConfigurationException( + $"Explicit construction for entity '{FormatType(entityType)}' binds property path '{binding.MemberPath}', but that path is not an active explicit mapping. Map the property path before passing it to ConstructUsing(...)."); + } + } } internal static void ValidateConvention(Type entityType, Convention convention) diff --git a/src/Dapper.FluentMap/MappingRegistry.cs b/src/Dapper.FluentMap/MappingRegistry.cs index 088e2f3..3593e1a 100644 --- a/src/Dapper.FluentMap/MappingRegistry.cs +++ b/src/Dapper.FluentMap/MappingRegistry.cs @@ -234,6 +234,18 @@ internal IPropertyMap GetProfilePropertyMap(Type type, Type profileType, string .PropertyMap; } + internal EntityConstructionStrategy GetConstructionStrategy(Type type, Type profileType) + { + IEntityMap entityMap; + var found = profileType == null + ? EntityMaps.TryGetValue(type, out entityMap) + : ProfileMaps.TryGetValue(new MappingProfileKey(type, profileType), out entityMap); + + return found + ? (entityMap as IEntityMapWithConstructionStrategy)?.ConstructionStrategy + : null; + } + internal IPropertyMap GetConventionPropertyMap(Type type, string columnName) { var cacheKey = MappingCacheKey.ConventionOnly(type, columnName); diff --git a/src/Dapper.FluentMap/Materialization/MappedRowMaterializer.cs b/src/Dapper.FluentMap/Materialization/MappedRowMaterializer.cs index d8670b5..2626428 100644 --- a/src/Dapper.FluentMap/Materialization/MappedRowMaterializer.cs +++ b/src/Dapper.FluentMap/Materialization/MappedRowMaterializer.cs @@ -33,32 +33,47 @@ internal static Func CreateMaterializer< Type profileType, FluentMapRuntime runtime) where TEntity : class + { + var materializer = CreateMaterializer(reader, typeof(TEntity), profileType, runtime); + return record => (TEntity)materializer(record); + } + + internal static Func CreateMaterializer( + IDataRecord reader, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] Type entityType, + Type profileType, + FluentMapRuntime runtime) { if (runtime == null) { throw new ArgumentNullException(nameof(runtime)); } + if (entityType == null) + { + throw new ArgumentNullException(nameof(entityType)); + } + var columnNames = GetColumnNames(reader); Func generatedMaterializer; var hasGeneratedMaterializer = runtime.StrictGeneratedMaterialization ? runtime.Registry.TryGetStrictGeneratedMaterializer( - typeof(TEntity), profileType, columnNames, out generatedMaterializer) + entityType, profileType, columnNames, out generatedMaterializer) : runtime.Registry.TryGetGeneratedMaterializer( - typeof(TEntity), profileType, columnNames, out generatedMaterializer); + entityType, profileType, columnNames, out generatedMaterializer); if (hasGeneratedMaterializer) { - return record => (TEntity)generatedMaterializer(record); + return generatedMaterializer; } if (runtime.StrictGeneratedMaterialization) { - ThrowMissingGeneratedMaterializer(profileType, runtime, columnNames); + ThrowMissingGeneratedMaterializer(entityType, profileType, runtime, columnNames); } - var plan = runtime.Registry.GetMaterializationPlan(typeof(TEntity), profileType, columnNames); - return record => (TEntity)plan.Materialize(record); + var plan = runtime.Registry.GetMaterializationPlan(entityType, profileType, columnNames); + return plan.Materialize; } internal static Func CreateGeneratedMaterializer( @@ -93,10 +108,19 @@ private static void ThrowMissingGeneratedMaterializer( FluentMapRuntime runtime, string[] columnNames) where TEntity : class + { + ThrowMissingGeneratedMaterializer(typeof(TEntity), profileType, runtime, columnNames); + } + + private static void ThrowMissingGeneratedMaterializer( + Type entityType, + Type profileType, + FluentMapRuntime runtime, + string[] columnNames) { throw new FluentMapConfigurationException( "Strict generated materialization is enabled, but FluentMap could not resolve a compatible generated materializer. " + - runtime.Registry.DescribeMissingGeneratedMaterializer(typeof(TEntity), profileType, columnNames) + + runtime.Registry.DescribeMissingGeneratedMaterializer(entityType, profileType, columnNames) + " Register a compatible generated materializer, query a documented generated shape, or use QueryMapped* with strict generated materialization disabled for the non-strict runtime fallback."); } diff --git a/src/Dapper.FluentMap/Materialization/NestedMaterializationPlan.cs b/src/Dapper.FluentMap/Materialization/NestedMaterializationPlan.cs index 5159b44..23d02d2 100644 --- a/src/Dapper.FluentMap/Materialization/NestedMaterializationPlan.cs +++ b/src/Dapper.FluentMap/Materialization/NestedMaterializationPlan.cs @@ -37,7 +37,9 @@ internal static NestedMaterializationPlan Create(Type entityType, Type profileTy } var defaultTypeMap = new DefaultTypeMap(entityType); - var rootNode = MaterializationNode.Root(entityType); + var rootNode = MaterializationNode.Root( + entityType, + registry.GetConstructionStrategy(entityType, profileType)); for (var i = 0; i < columnNames.Count; i++) { @@ -436,15 +438,22 @@ private sealed class MaterializationNode private int[] _subtreeColumnIndexes; private Func _parameterlessFactory; private ConstructorPlan _constructorPlan; + private ExplicitFactoryPlan _explicitFactoryPlan; private NestedLeaf[] _postConstructorLeaves; private MaterializationNode[] _postConstructorChildren; - private MaterializationNode(Type type, PropertyInfo parentProperty, string memberPath, bool isRoot) + private MaterializationNode( + Type type, + PropertyInfo parentProperty, + string memberPath, + bool isRoot, + EntityConstructionStrategy constructionStrategy = null) { Type = type; ParentProperty = parentProperty; MemberPath = memberPath; _isRoot = isRoot; + ConstructionStrategy = constructionStrategy; if (parentProperty != null) { @@ -463,11 +472,13 @@ private MaterializationNode(Type type, PropertyInfo parentProperty, string membe internal Action Setter { get; } + private EntityConstructionStrategy ConstructionStrategy { get; } + internal bool CanAssignToParent => _isRoot || Setter != null; - internal static MaterializationNode Root(Type type) + internal static MaterializationNode Root(Type type, EntityConstructionStrategy constructionStrategy) { - return new MaterializationNode(type, null, type.Name, isRoot: true); + return new MaterializationNode(type, null, type.Name, isRoot: true, constructionStrategy); } internal void AddPropertyPath( @@ -573,7 +584,7 @@ internal void Apply(object parent, IDataRecord record) return; } - var existing = _constructorPlan == null && Getter != null + var existing = _constructorPlan == null && _explicitFactoryPlan == null && Getter != null ? Getter(parent) : null; var value = Materialize(record, existing, forceNew: false); @@ -604,6 +615,12 @@ private void SelectConstructionPlan(Type entityType) { _parameterlessFactory = CreateParameterlessFactory(Type); + if (ConstructionStrategy != null) + { + SelectExplicitFactoryPlan(entityType); + return; + } + var requiresConstructor = _parameterlessFactory == null || _leaves.Any(leaf => !leaf.CanAssign) || _children.Any(child => !child.CanAssignToParent); @@ -644,6 +661,61 @@ private void SelectConstructionPlan(Type entityType) } } + private void SelectExplicitFactoryPlan(Type entityType) + { + var bindings = new List(); + foreach (var configuredBinding in ConstructionStrategy.Bindings) + { + var leaf = _leaves.SingleOrDefault(candidate => + string.Equals(candidate.MemberPath, configuredBinding.MemberPath.ToString(), StringComparison.Ordinal)); + var child = _children.SingleOrDefault(candidate => + string.Equals(candidate.MemberPath, configuredBinding.MemberPath.ToString(), StringComparison.Ordinal)); + + if (leaf == null && child == null) + { + throw new FluentMapConfigurationException( + $"Explicit construction for entity '{FormatType(entityType)}' requires mapped property path '{configuredBinding.MemberPath}', but the current result shape does not contain its mapped column."); + } + + var materializedType = leaf == null ? child.Type : leaf.TargetType; + if (!IsParameterCompatible(configuredBinding.ValueType, materializedType)) + { + throw new FluentMapConfigurationException( + $"Explicit construction for entity '{FormatType(entityType)}' binds property path '{configuredBinding.MemberPath}' as '{FormatType(configuredBinding.ValueType)}', but materialization produces '{FormatType(materializedType)}'."); + } + + bindings.Add(leaf == null + ? ExplicitFactoryBinding.ForChild(configuredBinding, child) + : ExplicitFactoryBinding.ForLeaf(configuredBinding, leaf)); + } + + _explicitFactoryPlan = new ExplicitFactoryPlan( + entityType, + MemberPath, + ConstructionStrategy.Factory, + bindings); + _postConstructorLeaves = _leaves + .Where(leaf => !_explicitFactoryPlan.Uses(leaf)) + .ToArray(); + _postConstructorChildren = _children + .Where(child => !_explicitFactoryPlan.Uses(child)) + .ToArray(); + + var unsupportedLeaf = _postConstructorLeaves.FirstOrDefault(leaf => !leaf.CanAssign); + if (unsupportedLeaf != null) + { + throw new FluentMapConfigurationException( + $"Explicit construction for entity '{FormatType(entityType)}' does not bind read-only mapped property '{unsupportedLeaf.MemberPath}'. Column: '{unsupportedLeaf.ColumnName}'."); + } + + var unsupportedChild = _postConstructorChildren.FirstOrDefault(child => !child.CanAssignToParent); + if (unsupportedChild != null) + { + throw new FluentMapConfigurationException( + $"Explicit construction for entity '{FormatType(entityType)}' does not bind read-only nested value object '{unsupportedChild.MemberPath}'."); + } + } + private ConstructorPlan SelectConstructor(Type entityType) { var candidates = Type.GetConstructors(BindingFlags.Public | BindingFlags.Instance) @@ -742,7 +814,9 @@ private ParameterBinding TryBindParameter(ParameterInfo parameter) private object Materialize(IDataRecord record, object existing, bool forceNew) { - var current = _constructorPlan != null + var current = _explicitFactoryPlan != null + ? _explicitFactoryPlan.Materialize(record) + : _constructorPlan != null ? _constructorPlan.Materialize(record) : forceNew || existing == null ? _parameterlessFactory() @@ -774,6 +848,91 @@ internal IEnumerable GetColumnNames() } } + private sealed class ExplicitFactoryPlan + { + private readonly Type _entityType; + private readonly string _memberPath; + private readonly Func _factory; + private readonly ExplicitFactoryBinding[] _bindings; + + internal ExplicitFactoryPlan( + Type entityType, + string memberPath, + Func factory, + IEnumerable bindings) + { + _entityType = entityType; + _memberPath = memberPath; + _factory = factory; + _bindings = bindings.ToArray(); + } + + internal bool Uses(NestedLeaf leaf) + { + return _bindings.Any(binding => binding.Leaf == leaf); + } + + internal bool Uses(MaterializationNode child) + { + return _bindings.Any(binding => binding.Child == child); + } + + internal object Materialize(IDataRecord record) + { + var values = _bindings.Select(binding => binding.GetValue(record)).ToArray(); + try + { + var entity = _factory(values); + if (entity == null) + { + throw new InvalidOperationException("The configured factory returned null."); + } + + return entity; + } + catch (Exception exception) + { + throw new FluentMapConfigurationException( + $"Explicit construction factory failed for entity '{FormatType(_entityType)}' at member path '{_memberPath}'. Bound property paths: {string.Join(", ", _bindings.Select(binding => "'" + binding.ConfiguredBinding.MemberPath + "'"))}. See the inner exception for the factory failure.", + exception); + } + } + } + + private sealed class ExplicitFactoryBinding + { + private ExplicitFactoryBinding( + ConstructionValueBinding configuredBinding, + NestedLeaf leaf, + MaterializationNode child) + { + ConfiguredBinding = configuredBinding; + Leaf = leaf; + Child = child; + } + + internal ConstructionValueBinding ConfiguredBinding { get; } + + internal NestedLeaf Leaf { get; } + + internal MaterializationNode Child { get; } + + internal static ExplicitFactoryBinding ForLeaf(ConstructionValueBinding binding, NestedLeaf leaf) + { + return new ExplicitFactoryBinding(binding, leaf, null); + } + + internal static ExplicitFactoryBinding ForChild(ConstructionValueBinding binding, MaterializationNode child) + { + return new ExplicitFactoryBinding(binding, null, child); + } + + internal object GetValue(IDataRecord record) + { + return Leaf == null ? Child.MaterializeValue(record) : Leaf.GetValue(record); + } + } + private sealed class NestedLeaf { private readonly Action _setter; diff --git a/src/Dapper.FluentMap/Properties/AssemblyInfo.cs b/src/Dapper.FluentMap/Properties/AssemblyInfo.cs index 6040a49..35e17e4 100644 --- a/src/Dapper.FluentMap/Properties/AssemblyInfo.cs +++ b/src/Dapper.FluentMap/Properties/AssemblyInfo.cs @@ -2,3 +2,4 @@ [assembly: InternalsVisibleTo("Dapper.FluentMap.Tests")] [assembly: InternalsVisibleTo("Dapper.FluentMap.GeneratedRegistration.Tests")] +[assembly: InternalsVisibleTo("Dapper.FluentMap.Dommel")] diff --git a/src/Dapper.FluentMap/QueryMappedExtensions.cs b/src/Dapper.FluentMap/QueryMappedExtensions.cs index c0b5e6e..c87dadb 100644 --- a/src/Dapper.FluentMap/QueryMappedExtensions.cs +++ b/src/Dapper.FluentMap/QueryMappedExtensions.cs @@ -679,6 +679,310 @@ public static IEnumerable QueryMapped< runtime: FluentMapper.Runtime); } + /// + /// Executes a query, splits each row into three FluentMap-controlled segments and composes the return value. + /// + /// + /// must contain two comma-separated, unique column names in result order. + /// An all-database-null second or third segment is passed to as . + /// This API composes one row at a time and does not aggregate one-to-many graphs. + /// + [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + public static IEnumerable QueryMapped< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn>( + this IDbConnection connection, + string sql, + Func map, + object param = null, + IDbTransaction transaction = null, + int? commandTimeout = null, + CommandType? commandType = null, + string splitOn = null) + where TFirst : class + where TSecond : class + where TThird : class + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + return QueryMapped( + connection, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType), + map, + splitOn); + } + + /// + /// Executes a command, splits each row into three FluentMap-controlled segments and composes the return value. + /// + [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + public static IEnumerable QueryMapped< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn>( + this IDbConnection connection, + CommandDefinition command, + Func map, + string splitOn = null) + where TFirst : class + where TSecond : class + where TThird : class + { + return ExecuteMapped( + connection, command, map, splitOn, null, null, null, FluentMapper.Runtime); + } + + /// + /// Executes a query and materializes three segments with an explicit mapping profile per segment. + /// + [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + public static IEnumerable QueryMapped< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn, + TFirstProfile, + TSecondProfile, + TThirdProfile>( + this IDbConnection connection, + string sql, + Func map, + object param = null, + IDbTransaction transaction = null, + int? commandTimeout = null, + CommandType? commandType = null, + string splitOn = null) + where TFirst : class + where TSecond : class + where TThird : class + where TFirstProfile : IMappingProfile + where TSecondProfile : IMappingProfile + where TThirdProfile : IMappingProfile + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + return ExecuteMapped( + connection, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType), + map, + splitOn, + typeof(TFirstProfile), + typeof(TSecondProfile), + typeof(TThirdProfile), + FluentMapper.Runtime); + } + + /// + /// Executes a command and materializes three segments with an explicit mapping profile per segment. + /// + [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + public static IEnumerable QueryMapped< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn, + TFirstProfile, + TSecondProfile, + TThirdProfile>( + this IDbConnection connection, + CommandDefinition command, + Func map, + string splitOn) + where TFirst : class + where TSecond : class + where TThird : class + where TFirstProfile : IMappingProfile + where TSecondProfile : IMappingProfile + where TThirdProfile : IMappingProfile + { + return ExecuteMapped( + connection, + command, + map, + splitOn, + typeof(TFirstProfile), + typeof(TSecondProfile), + typeof(TThirdProfile), + FluentMapper.Runtime); + } + + /// + /// Asynchronously executes a query, materializes three FluentMap-controlled segments and composes the return value. + /// + [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + public static Task> QueryMappedAsync< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn>( + this IDbConnection connection, + string sql, + Func map, + object param = null, + IDbTransaction transaction = null, + int? commandTimeout = null, + CommandType? commandType = null, + string splitOn = null, + CancellationToken cancellationToken = default) + where TFirst : class + where TSecond : class + where TThird : class + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + if (map == null) + { + throw new ArgumentNullException(nameof(map)); + } + + return ExecuteMappedSegmentsAsync( + connection, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, cancellationToken), + new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, + new Type[] { null, null, null }, + values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + splitOn, + FluentMapper.Runtime); + } + + /// + /// Asynchronously executes a command, materializes three FluentMap-controlled segments and composes the return value. + /// + [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + public static Task> QueryMappedAsync< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn>( + this IDbConnection connection, + CommandDefinition command, + Func map, + string splitOn) + where TFirst : class + where TSecond : class + where TThird : class + { + if (map == null) + { + throw new ArgumentNullException(nameof(map)); + } + + return ExecuteMappedSegmentsAsync( + connection, + command, + new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, + new Type[] { null, null, null }, + values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + splitOn, + FluentMapper.Runtime); + } + + /// + /// Asynchronously executes a query and materializes three segments with an explicit mapping profile per segment. + /// + [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + public static Task> QueryMappedAsync< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn, + TFirstProfile, + TSecondProfile, + TThirdProfile>( + this IDbConnection connection, + string sql, + Func map, + object param = null, + IDbTransaction transaction = null, + int? commandTimeout = null, + CommandType? commandType = null, + string splitOn = null, + CancellationToken cancellationToken = default) + where TFirst : class + where TSecond : class + where TThird : class + where TFirstProfile : IMappingProfile + where TSecondProfile : IMappingProfile + where TThirdProfile : IMappingProfile + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + if (map == null) + { + throw new ArgumentNullException(nameof(map)); + } + + return ExecuteMappedSegmentsAsync( + connection, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, cancellationToken), + new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, + new[] { typeof(TFirstProfile), typeof(TSecondProfile), typeof(TThirdProfile) }, + values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + splitOn, + FluentMapper.Runtime); + } + + /// + /// Asynchronously executes a command and materializes three segments with an explicit mapping profile per segment. + /// + [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + public static Task> QueryMappedAsync< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn, + TFirstProfile, + TSecondProfile, + TThirdProfile>( + this IDbConnection connection, + CommandDefinition command, + Func map, + string splitOn) + where TFirst : class + where TSecond : class + where TThird : class + where TFirstProfile : IMappingProfile + where TSecondProfile : IMappingProfile + where TThirdProfile : IMappingProfile + { + if (map == null) + { + throw new ArgumentNullException(nameof(map)); + } + + return ExecuteMappedSegmentsAsync( + connection, + command, + new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, + new[] { typeof(TFirstProfile), typeof(TSecondProfile), typeof(TThirdProfile) }, + values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + splitOn, + FluentMapper.Runtime); + } + /// /// Executes a query, splits each row at , materializes two profiled FluentMap-controlled segments and composes the return value. /// @@ -958,19 +1262,65 @@ internal static IEnumerable ExecuteMapped< where TFirst : class where TSecond : class { - if (connection == null) + if (map == null) { - throw new ArgumentNullException(nameof(connection)); + throw new ArgumentNullException(nameof(map)); } + return ExecuteMappedSegments( + connection, + command, + new[] { typeof(TFirst), typeof(TSecond) }, + new[] { firstProfileType, secondProfileType }, + values => map((TFirst)values[0], (TSecond)values[1]), + splitOn, + runtime); + } + + internal static IEnumerable ExecuteMapped< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn>( + IDbConnection connection, + CommandDefinition command, + Func map, + string splitOn, + Type firstProfileType, + Type secondProfileType, + Type thirdProfileType, + FluentMapRuntime runtime) + where TFirst : class + where TSecond : class + where TThird : class + { if (map == null) { throw new ArgumentNullException(nameof(map)); } - if (string.IsNullOrWhiteSpace(splitOn)) + return ExecuteMappedSegments( + connection, + command, + new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, + new[] { firstProfileType, secondProfileType, thirdProfileType }, + values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + splitOn, + runtime); + } + + private static IEnumerable ExecuteMappedSegments( + IDbConnection connection, + CommandDefinition command, + Type[] entityTypes, + Type[] profileTypes, + Func map, + string splitOn, + FluentMapRuntime runtime) + { + if (connection == null) { - throw new ArgumentException("A splitOn column name is required.", nameof(splitOn)); + throw new ArgumentNullException(nameof(connection)); } if (runtime == null) @@ -980,18 +1330,47 @@ internal static IEnumerable ExecuteMapped< using (var reader = SqlMapper.ExecuteReader(connection, command)) { - var splitIndex = GetSplitIndex(reader, splitOn); - var firstSegment = new SegmentDataRecord(reader, 0, splitIndex); - var secondSegment = new SegmentDataRecord(reader, splitIndex, reader.FieldCount - splitIndex); - var firstMaterializer = MappedRowMaterializer.CreateMaterializer(firstSegment, firstProfileType, runtime); - var secondMaterializer = MappedRowMaterializer.CreateMaterializer(secondSegment, secondProfileType, runtime); + var segments = CreateSegments(reader, splitOn, entityTypes.Length); + var materializers = CreateSegmentMaterializers(segments, entityTypes, profileTypes, runtime); var results = new List(); while (reader.Read()) { - var first = firstMaterializer(firstSegment); - var second = IsAllNull(secondSegment) ? null : secondMaterializer(secondSegment); - results.Add(map(first, second)); + results.Add(map(MaterializeSegments(segments, materializers))); + } + + return results; + } + } + + internal static async Task> ExecuteMappedSegmentsAsync( + IDbConnection connection, + CommandDefinition command, + Type[] entityTypes, + Type[] profileTypes, + Func map, + string splitOn, + FluentMapRuntime runtime) + { + if (connection == null) + { + throw new ArgumentNullException(nameof(connection)); + } + + if (runtime == null) + { + throw new ArgumentNullException(nameof(runtime)); + } + + using (var reader = await SqlMapper.ExecuteReaderAsync(connection, command).ConfigureAwait(false)) + { + var segments = CreateSegments(reader, splitOn, entityTypes.Length); + var materializers = CreateSegmentMaterializers(segments, entityTypes, profileTypes, runtime); + var results = new List(); + + while (reader.Read()) + { + results.Add(map(MaterializeSegments(segments, materializers))); } return results; @@ -1017,35 +1396,94 @@ internal static async Task ExecuteMultipleMappedAsync( return new MappedGridReader(reader, runtime); } - private static int GetSplitIndex(IDataRecord reader, string splitOn) + private static SegmentDataRecord[] CreateSegments(IDataRecord reader, string splitOn, int segmentCount) { - var splitIndex = -1; - for (var i = 0; i < reader.FieldCount; i++) + if (string.IsNullOrWhiteSpace(splitOn)) + { + throw new ArgumentException("One splitOn column name is required for each segment after the first.", nameof(splitOn)); + } + + var boundaries = splitOn.Split(',').Select(boundary => boundary.Trim()).ToArray(); + if (boundaries.Any(string.IsNullOrEmpty)) + { + throw new ArgumentException("splitOn cannot contain an empty boundary.", nameof(splitOn)); + } + + if (boundaries.Length != segmentCount - 1) { - if (!string.Equals(reader.GetName(i), splitOn, StringComparison.OrdinalIgnoreCase)) + throw new ArgumentException( + $"Multi-mapping with {segmentCount} segments requires exactly {segmentCount - 1} splitOn boundaries; {boundaries.Length} were provided.", + nameof(splitOn)); + } + + var duplicateBoundary = boundaries + .GroupBy(boundary => boundary, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(group => group.Count() > 1); + if (duplicateBoundary != null) + { + throw new InvalidOperationException( + "The splitOn boundary '" + duplicateBoundary.Key + "' is duplicated and ambiguous."); + } + + var indexes = new List { 0 }; + foreach (var boundary in boundaries) + { + var matches = Enumerable.Range(0, reader.FieldCount) + .Where(index => string.Equals(reader.GetName(index), boundary, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + if (matches.Length == 0) { - continue; + throw new InvalidOperationException("The splitOn column '" + boundary + "' was not found in the result set."); } - if (splitIndex >= 0) + if (matches.Length > 1) { - throw new InvalidOperationException("The splitOn column '" + splitOn + "' is ambiguous in the result set."); + throw new InvalidOperationException("The splitOn column '" + boundary + "' is ambiguous in the result set."); } - splitIndex = i; - } + if (matches[0] <= indexes[indexes.Count - 1]) + { + throw new InvalidOperationException( + "The splitOn column '" + boundary + "' is out of order or produced an empty row segment."); + } - if (splitIndex < 0) - { - throw new InvalidOperationException("The splitOn column '" + splitOn + "' was not found in the result set."); + indexes.Add(matches[0]); } - if (splitIndex == 0 || splitIndex >= reader.FieldCount) + indexes.Add(reader.FieldCount); + return Enumerable.Range(0, segmentCount) + .Select(index => new SegmentDataRecord( + reader, + indexes[index], + indexes[index + 1] - indexes[index])) + .ToArray(); + } + + private static Func[] CreateSegmentMaterializers( + SegmentDataRecord[] segments, + Type[] entityTypes, + Type[] profileTypes, + FluentMapRuntime runtime) + { + return Enumerable.Range(0, segments.Length) + .Select(index => MappedRowMaterializer.CreateMaterializer( + segments[index], entityTypes[index], profileTypes[index], runtime)) + .ToArray(); + } + + private static object[] MaterializeSegments( + SegmentDataRecord[] segments, + Func[] materializers) + { + var values = new object[segments.Length]; + for (var index = 0; index < segments.Length; index++) { - throw new InvalidOperationException("The splitOn column '" + splitOn + "' produced an empty row segment."); + values[index] = index > 0 && IsAllNull(segments[index]) + ? null + : materializers[index](segments[index]); } - return splitIndex; + return values; } private static bool IsAllNull(IDataRecord record) diff --git a/test/Dapper.FluentMap.Analyzers.Tests/FluentMapConfigurationAnalyzerTests.cs b/test/Dapper.FluentMap.Analyzers.Tests/FluentMapConfigurationAnalyzerTests.cs index de7cf8d..a4a76a3 100644 --- a/test/Dapper.FluentMap.Analyzers.Tests/FluentMapConfigurationAnalyzerTests.cs +++ b/test/Dapper.FluentMap.Analyzers.Tests/FluentMapConfigurationAnalyzerTests.cs @@ -545,6 +545,36 @@ public CustomerMap() AssertDiagnosticLineContains(source, diagnostic, "ConvertToDatabaseUsing()"); } + [Fact] + public async Task DuplicateConstructionStrategyShouldReportDfm016() + { + var source = @" +using Dapper.FluentMap.Mapping; + +public sealed class Customer +{ + public Customer(int id) { Id = id; } + public int Id { get; } +} + +public sealed class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(c => c.Id).ToColumn(""customer_id""); + ConstructUsing(c => c.Id, id => new Customer(id)); + ConstructUsing(c => c.Id, id => new Customer(id)); + } +}"; + + var diagnostic = await GetSingleDiagnosticAsync( + source, + FluentMapConfigurationAnalyzer.DuplicateConstructionStrategyDiagnosticId); + + AssertDiagnostic(diagnostic, DiagnosticSeverity.Error, "more than one ConstructUsing"); + AssertDiagnosticLineContains(source, diagnostic, "ConstructUsing(c => c.Id, id => new Customer(id))"); + } + [Fact] public async Task ValidMappingConfigurationShouldNotReportDiagnostics() { diff --git a/test/Dapper.FluentMap.Dommel.Tests/DommelPersistenceIntegrationTests.cs b/test/Dapper.FluentMap.Dommel.Tests/DommelPersistenceIntegrationTests.cs index 2df4f39..23d10c5 100644 --- a/test/Dapper.FluentMap.Dommel.Tests/DommelPersistenceIntegrationTests.cs +++ b/test/Dapper.FluentMap.Dommel.Tests/DommelPersistenceIntegrationTests.cs @@ -335,6 +335,176 @@ code TEXT NOT NULL } } + [Fact] + public void InsertMappedAndUpdateMappedShouldExecuteWriteConverters() + { + PreTest(); + SQLitePCL.Batteries_V2.Init(); + + FluentMapper.Initialize(config => + { + config.AddMap(new WriteConvertedEntityMap()); + config.ForDommel(); + }); + + using (var connection = OpenConnection()) + { + connection.Execute(@" +CREATE TABLE write_converted_entities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT NOT NULL, + insert_excluded TEXT, + update_excluded TEXT, + default_value TEXT DEFAULT 'database-default', + computed_value TEXT +);"); + + var logs = CaptureDommelLogs(); + var entity = new WriteConvertedEntity + { + Code = "insert", + InsertExcluded = "must-not-insert", + UpdateExcluded = "insert-only", + DefaultValue = "must-use-default", + ComputedValue = "must-not-write" + }; + + var id = Convert.ToInt32(connection.InsertMapped(entity)); + var insertSql = logs.Last(log => log.IndexOf("insert into", StringComparison.OrdinalIgnoreCase) >= 0); + AssertSqlDoesNotContain(insertSql, "insert_excluded", "default_value", "computed_value"); + var inserted = connection.QuerySingle(@" +SELECT id AS Id, code AS Code, insert_excluded AS InsertExcluded, + update_excluded AS UpdateExcluded, default_value AS DefaultValue, + computed_value AS ComputedValue +FROM write_converted_entities WHERE id = @id;", new { id }); + + Assert.Equal("db:insert", inserted.Code); + Assert.Null(inserted.InsertExcluded); + Assert.Equal("db:insert-only", inserted.UpdateExcluded); + Assert.Equal("database-default", inserted.DefaultValue); + Assert.Null(inserted.ComputedValue); + + entity.Id = id; + entity.Code = "update"; + entity.InsertExcluded = "update-only"; + entity.UpdateExcluded = "must-not-update"; + + Assert.True(connection.UpdateMapped(entity)); + + var updated = connection.QuerySingle(@" +SELECT id AS Id, code AS Code, insert_excluded AS InsertExcluded, + update_excluded AS UpdateExcluded, default_value AS DefaultValue, + computed_value AS ComputedValue +FROM write_converted_entities WHERE id = @id;", new { id }); + Assert.Equal("db:update", updated.Code); + Assert.Equal("db:update-only", updated.InsertExcluded); + Assert.Equal("db:insert-only", updated.UpdateExcluded); + Assert.Equal("db:must-use-default", updated.DefaultValue); + Assert.Null(updated.ComputedValue); + } + } + + [Fact] + public void WriteConverterFailureShouldIncludeDommelOperationAndPropertyContext() + { + PreTest(); + SQLitePCL.Batteries_V2.Init(); + + FluentMapper.Initialize(config => + { + config.AddMap(new WriteConversionBoundaryEntityMap()); + config.ForDommel(); + }); + + using (var connection = OpenConnection()) + { + connection.Execute(@" +CREATE TABLE write_conversion_boundary_entities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT NOT NULL +);"); + + var exception = Assert.Throws(() => + connection.InsertMapped(new WriteConversionBoundaryEntity { Code = "reject" })); + + Assert.Contains("INSERT", exception.Message); + Assert.Contains(typeof(WriteConversionBoundaryEntity).FullName, exception.Message); + Assert.Contains(nameof(WriteConversionBoundaryEntity.Code), exception.Message); + Assert.Contains("code", exception.Message); + Assert.IsType(exception.InnerException); + Assert.Equal(0, connection.QuerySingle("SELECT COUNT(*) FROM write_conversion_boundary_entities;")); + } + } + + [Fact] + public void ConverterOutputShouldThenUseDapperTypeHandler() + { + PreTest(); + SQLitePCL.Batteries_V2.Init(); + SqlMapper.AddTypeHandler(new ConvertedCodeTypeHandler()); + + FluentMapper.Initialize(config => + { + config.AddMap(new TypeHandledWriteEntityMap()); + config.ForDommel(); + }); + + using (var connection = OpenConnection()) + { + connection.Execute(@" +CREATE TABLE type_handled_write_entities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT NOT NULL +);"); + + var id = Convert.ToInt32(connection.InsertMapped(new TypeHandledWriteEntity { Code = "value" })); + + Assert.Equal( + "handler:converted:value", + connection.QuerySingle( + "SELECT code FROM type_handled_write_entities WHERE id = @id;", + new { id })); + } + } + + [Fact] + public async System.Threading.Tasks.Task AsyncMappedWritesShouldApplyWriteConverters() + { + PreTest(); + SQLitePCL.Batteries_V2.Init(); + + FluentMapper.Initialize(config => + { + config.AddMap(new WriteConversionBoundarySuccessEntityMap()); + config.ForDommel(); + }); + + using (var connection = OpenConnection()) + { + connection.Execute(@" +CREATE TABLE write_conversion_boundary_entities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT NOT NULL +);"); + var entity = new WriteConversionBoundaryEntity { Code = "async-insert" }; + var id = Convert.ToInt32(await connection.InsertMappedAsync( + entity, + cancellationToken: TestContext.Current.CancellationToken)); + + entity.Id = id; + entity.Code = "async-update"; + Assert.True(await connection.UpdateMappedAsync( + entity, + cancellationToken: TestContext.Current.CancellationToken)); + + Assert.Equal( + "db:async-update", + connection.QuerySingle( + "SELECT code FROM write_conversion_boundary_entities WHERE id = @id;", + new { id })); + } + } + private static void PreTest() { FluentMapper.EntityMaps.Clear(); @@ -541,5 +711,103 @@ public string ConvertToDatabase(string value) throw new InvalidOperationException("Dommel does not execute property write converters in this stage."); } } + + private sealed class WriteConversionBoundarySuccessEntityMap : DommelEntityMap + { + public WriteConversionBoundarySuccessEntityMap() + { + ToTable("write_conversion_boundary_entities"); + Map(entity => entity.Id).ToColumn("id").IsIdentity(); + Map(entity => entity.Code) + .ToColumn("code") + .ConvertToDatabaseUsing(value => "db:" + value); + } + } + + private sealed class WriteConvertedEntity + { + public int Id { get; set; } + + public string Code { get; set; } + + public string InsertExcluded { get; set; } + + public string UpdateExcluded { get; set; } + + public string DefaultValue { get; set; } + + public string ComputedValue { get; set; } + } + + private sealed class WriteConvertedEntityMap : DommelEntityMap + { + public WriteConvertedEntityMap() + { + ToTable("write_converted_entities"); + Map(entity => entity.Id).ToColumn("id").IsIdentity(); + Map(entity => entity.Code).ToColumn("code").ConvertToDatabaseUsing(value => "db:" + value); + Map(entity => entity.InsertExcluded).ToColumn("insert_excluded").ExcludeFromInsert().ConvertToDatabaseUsing(value => "db:" + value); + Map(entity => entity.UpdateExcluded).ToColumn("update_excluded").ExcludeFromUpdate().ConvertToDatabaseUsing(value => "db:" + value); + Map(entity => entity.DefaultValue).ToColumn("default_value").DatabaseDefaultOnInsert().ConvertToDatabaseUsing(value => "db:" + value); + Map(entity => entity.ComputedValue).ToColumn("computed_value").Computed(); + } + } + + private sealed class WriteConvertedRow + { + public int Id { get; set; } + + public string Code { get; set; } + + public string InsertExcluded { get; set; } + + public string UpdateExcluded { get; set; } + + public string DefaultValue { get; set; } + + public string ComputedValue { get; set; } + } + + private sealed class TypeHandledWriteEntity + { + public int Id { get; set; } + + public string Code { get; set; } + } + + private sealed class TypeHandledWriteEntityMap : DommelEntityMap + { + public TypeHandledWriteEntityMap() + { + ToTable("type_handled_write_entities"); + Map(entity => entity.Id).ToColumn("id").IsIdentity(); + Map(entity => entity.Code) + .ToColumn("code") + .ConvertToDatabaseUsing(value => new ConvertedCode("converted:" + value)); + } + } + + private sealed class ConvertedCode + { + public ConvertedCode(string value) + { + Value = value; + } + + public string Value { get; } + } + + private sealed class ConvertedCodeTypeHandler : SqlMapper.TypeHandler + { + public override ConvertedCode Parse(object value) + { + return new ConvertedCode(Convert.ToString(value)); + } + + public override void SetValue(System.Data.IDbDataParameter parameter, ConvertedCode value) + { + parameter.Value = "handler:" + value.Value; + } + } } } diff --git a/test/Dapper.FluentMap.Generators.Tests/MappingRegistrationGeneratorTests.cs b/test/Dapper.FluentMap.Generators.Tests/MappingRegistrationGeneratorTests.cs index 988ba99..b64dc6e 100644 --- a/test/Dapper.FluentMap.Generators.Tests/MappingRegistrationGeneratorTests.cs +++ b/test/Dapper.FluentMap.Generators.Tests/MappingRegistrationGeneratorTests.cs @@ -736,6 +736,38 @@ public CustomerMap() Assert.DoesNotContain(".AddGeneratedMaterializer(", result.GeneratedSource, StringComparison.Ordinal); } + [Fact] + public void ExplicitConstructionStrategyShouldRegisterMapAndReportRuntimeOnlyBoundary() + { + var source = @" +using Dapper.FluentMap.Mapping; + +public sealed class Customer +{ + private Customer(int id) { Id = id; } + public int Id { get; } + public static Customer Restore(int id) => new Customer(id); +} + +public sealed class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(customer => customer.Id).ToColumn(""customer_id""); + ConstructUsing(customer => customer.Id, Customer.Restore); + } +}"; + + var result = RunGenerator(source); + var diagnostic = Assert.Single(result.DfmDiagnostics); + + Assert.Equal(MappingRegistrationGenerator.SkippedGeneratedMaterializerDiagnosticId, diagnostic.Id); + Assert.Contains("ConstructUsing", diagnostic.GetMessage(), StringComparison.Ordinal); + Assert.Contains("runtime materialization", diagnostic.GetMessage(), StringComparison.Ordinal); + Assert.Contains(".AddMap()", result.GeneratedSource, StringComparison.Ordinal); + Assert.DoesNotContain(".AddGeneratedMaterializer(", result.GeneratedSource, StringComparison.Ordinal); + } + [Fact] public void DuplicateProfileMappingsShouldReportDiagnostic() { diff --git a/test/Dapper.FluentMap.ProviderCompatibility.Tests/ProviderCompatibilityTests.cs b/test/Dapper.FluentMap.ProviderCompatibility.Tests/ProviderCompatibilityTests.cs index 21068af..d7a66ce 100644 --- a/test/Dapper.FluentMap.ProviderCompatibility.Tests/ProviderCompatibilityTests.cs +++ b/test/Dapper.FluentMap.ProviderCompatibility.Tests/ProviderCompatibilityTests.cs @@ -380,6 +380,44 @@ public void QueryMappedShouldComposeProviderJoinRows(string providerName) } } + [Theory] + [MemberData(nameof(Providers))] + [Trait("Category", "ProviderCompatibility")] + public void QueryMappedShouldComposeThreeProviderSegments(string providerName) + { + var provider = ProviderCase.Create(providerName); + provider.SkipIfUnavailable(); + PreTest(typeof(MultipleCustomer), typeof(MultipleOrder), typeof(MultipleShipment)); + + try + { + FluentMapper.Initialize(configuration => + { + configuration.AddMap(new MultipleCustomerMap()); + configuration.AddMap(new MultipleOrderMap()); + configuration.AddMap(new MultipleShipmentMap()); + }); + + using (var connection = provider.OpenConnection()) + { + var row = connection.QueryMapped>( + provider.ThreeSegmentMultiMappingSql(), + (customer, order, shipment) => Tuple.Create(customer, order, shipment), + splitOn: "order_id,shipment_id") + .Single(); + + Assert.Equal(11, row.Item1.Id); + Assert.Equal(99, row.Item2.Id); + Assert.Equal(501, row.Item3.Id); + Assert.Equal("sent", row.Item3.Status); + } + } + finally + { + PreTest(typeof(MultipleCustomer), typeof(MultipleOrder), typeof(MultipleShipment)); + } + } + [Theory] [MemberData(nameof(Providers))] [Trait("Category", "ProviderCompatibility")] @@ -524,13 +562,13 @@ public void DommelPersistenceShouldHonorGeneratedDefaultsAndReadOnlyMetadata(str Computed = "client-computed" }; - var id = Convert.ToInt32(connection.Insert(entity)); + var id = Convert.ToInt32(connection.InsertMapped(entity)); var inserted = connection.Get(id); - Assert.Equal("inserted", inserted.Normal); + Assert.Equal("db:inserted", inserted.Normal); Assert.Equal("read-only-default", inserted.ReadOnly); Assert.Equal("default-value-default", inserted.DefaultValue); - Assert.Equal("inserted-computed", inserted.Computed); + Assert.Equal("db:inserted-computed", inserted.Computed); entity.Id = id; entity.Normal = "updated"; @@ -538,13 +576,13 @@ public void DommelPersistenceShouldHonorGeneratedDefaultsAndReadOnlyMetadata(str entity.DefaultValue = "updated-default"; entity.Computed = "updated-computed"; - Assert.True(connection.Update(entity)); + Assert.True(connection.UpdateMapped(entity)); var updated = connection.Get(id); - Assert.Equal("updated", updated.Normal); + Assert.Equal("db:updated", updated.Normal); Assert.Equal("read-only-default", updated.ReadOnly); Assert.Equal("updated-default", updated.DefaultValue); - Assert.Equal("updated-computed", updated.Computed); + Assert.Equal("db:updated-computed", updated.Computed); } } finally @@ -923,6 +961,13 @@ public string MultiMappingSql() "99 AS order_id, " + DecimalLiteral(12.34m) + " AS total;"; } + public string ThreeSegmentMultiMappingSql() + { + return "SELECT 11 AS customer_id, " + TextLiteral("Multiple") + " AS customer_name, " + + "99 AS order_id, " + DecimalLiteral(12.34m) + " AS total, " + + "501 AS shipment_id, " + TextLiteral("sent") + " AS shipment_status;"; + } + public string SingleCustomerSql(int id, string name) { return "SELECT " + Literal(id) + " AS customer_id, " + TextLiteral(name) + " AS customer_name;"; @@ -1176,6 +1221,22 @@ public MultipleOrderMap() } } + private sealed class MultipleShipment + { + public int Id { get; set; } + + public string Status { get; set; } + } + + private sealed class MultipleShipmentMap : EntityMap + { + public MultipleShipmentMap() + { + Map(shipment => shipment.Id).ToColumn("shipment_id"); + Map(shipment => shipment.Status).ToColumn("shipment_status"); + } + } + private sealed class StreamCustomer { public int Id { get; set; } @@ -1213,7 +1274,9 @@ public ProviderPersistenceEntityMap() { ToTable(MappedTableName); Map(entity => entity.Id).ToColumn("id").IsIdentity(); - Map(entity => entity.Normal).ToColumn("normal"); + Map(entity => entity.Normal) + .ToColumn("normal") + .ConvertToDatabaseUsing(value => "db:" + value); Map(entity => entity.ReadOnly).ToColumn("read_only").ReadOnly(); Map(entity => entity.DefaultValue).ToColumn("default_value").DatabaseDefaultOnInsert(); Map(entity => entity.Computed).ToColumn("computed").Computed(); diff --git a/test/Dapper.FluentMap.Tests/ExplicitConstructionStrategyTests.cs b/test/Dapper.FluentMap.Tests/ExplicitConstructionStrategyTests.cs new file mode 100644 index 0000000..60773f0 --- /dev/null +++ b/test/Dapper.FluentMap.Tests/ExplicitConstructionStrategyTests.cs @@ -0,0 +1,204 @@ +using System; +using Dapper.FluentMap.Configuration; +using Dapper.FluentMap.Mapping; +using Microsoft.Data.Sqlite; +using Xunit; + +namespace Dapper.FluentMap.Tests +{ + public class ExplicitConstructionStrategyTests + { + [Fact] + [Trait("Category", "Integration")] + public void QueryMappedShouldUseExplicitFactoryForPrivateConstructor() + { + FluentMapper.Reset(typeof(FactoryCustomer)); + + try + { + FluentMapper.Initialize(configuration => configuration.AddMap(new FactoryCustomerMap())); + + using (var connection = OpenConnection()) + { + var customer = connection.QueryMappedSingle( + "SELECT 17 AS customer_id, 'Ada' AS customer_name;"); + + Assert.Equal(17, customer.Id); + Assert.Equal("Ada", customer.Name); + Assert.True(customer.CreatedByFactory); + } + } + finally + { + FluentMapper.Reset(typeof(FactoryCustomer)); + } + } + + [Fact] + [Trait("Category", "Integration")] + public void IsolatedRuntimeShouldPreserveExplicitFactory() + { + var runtime = new FluentMapConfigurationBuilder() + .AddMap(new FactoryCustomerMap()) + .Build() + .CreateRuntime(); + + using (var connection = OpenConnection()) + { + var customer = runtime.QueryMappedSingle( + connection, + "SELECT 18 AS customer_id, 'Grace' AS customer_name;"); + + Assert.Equal(18, customer.Id); + Assert.Equal("Grace", customer.Name); + Assert.True(customer.CreatedByFactory); + } + } + + [Fact] + public void ValidationShouldRejectConstructionBindingWithoutActiveExplicitMap() + { + FluentMapper.Reset(typeof(FactoryCustomer)); + + try + { + var exception = Assert.Throws(() => + FluentMapper.Initialize(configuration => configuration.AddMap(new IncompleteFactoryCustomerMap()))); + + Assert.Contains(nameof(FactoryCustomer.Name), exception.Message); + Assert.Contains("ConstructUsing", exception.Message); + } + finally + { + FluentMapper.Reset(typeof(FactoryCustomer)); + } + } + + [Fact] + public void DuplicateConstructionStrategyShouldFailDuringMapConfiguration() + { + var exception = Assert.Throws(() => new DuplicateFactoryCustomerMap()); + + Assert.Contains(typeof(FactoryCustomer).FullName, exception.Message); + Assert.Contains("already has", exception.Message); + } + + [Fact] + [Trait("Category", "Integration")] + public void FactoryFailureShouldIncludeEntityAndBoundPropertyContext() + { + var runtime = new FluentMapConfigurationBuilder() + .AddMap(new ThrowingFactoryCustomerMap()) + .Build() + .CreateRuntime(); + + using (var connection = OpenConnection()) + { + var exception = Assert.Throws(() => + runtime.QueryMappedSingle( + connection, + "SELECT 19 AS customer_id, 'Failure' AS customer_name;")); + + Assert.Contains(typeof(FactoryCustomer).FullName, exception.Message); + Assert.Contains(nameof(FactoryCustomer.Id), exception.Message); + Assert.Contains(nameof(FactoryCustomer.Name), exception.Message); + Assert.IsType(exception.InnerException); + } + } + + [Fact] + [Trait("Category", "Integration")] + public void StrictGeneratedRuntimeShouldRejectRuntimeOnlyFactoryWithoutFallback() + { + var runtime = new FluentMapConfigurationBuilder() + .AddMap(new FactoryCustomerMap()) + .UseStrictGeneratedMaterialization() + .Build() + .CreateRuntime(); + + using (var connection = OpenConnection()) + { + var exception = Assert.Throws(() => + runtime.QueryMappedSingle( + connection, + "SELECT 20 AS customer_id, 'Strict' AS customer_name;")); + + Assert.Contains("Strict generated materialization", exception.Message); + Assert.Contains("runtime fallback", exception.Message); + } + } + + private static SqliteConnection OpenConnection() + { + var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + return connection; + } + + private sealed class FactoryCustomer + { + private FactoryCustomer(int id, string name) + { + Id = id; + Name = name; + CreatedByFactory = true; + } + + public int Id { get; } + + public string Name { get; } + + public bool CreatedByFactory { get; } + + internal static FactoryCustomer Restore(int id, string name) + { + return new FactoryCustomer(id, name); + } + + internal static FactoryCustomer RestoreIdOnly(int id) + { + return new FactoryCustomer(id, string.Empty); + } + } + + private class FactoryCustomerMap : EntityMap + { + public FactoryCustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Name).ToColumn("customer_name"); + ConstructUsing(customer => customer.Id, customer => customer.Name, FactoryCustomer.Restore); + } + } + + private sealed class IncompleteFactoryCustomerMap : EntityMap + { + public IncompleteFactoryCustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + ConstructUsing(customer => customer.Id, customer => customer.Name, FactoryCustomer.Restore); + } + } + + private sealed class DuplicateFactoryCustomerMap : FactoryCustomerMap + { + public DuplicateFactoryCustomerMap() + { + ConstructUsing(customer => customer.Id, FactoryCustomer.RestoreIdOnly); + } + } + + private sealed class ThrowingFactoryCustomerMap : EntityMap + { + public ThrowingFactoryCustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + Map(customer => customer.Name).ToColumn("customer_name"); + ConstructUsing( + customer => customer.Id, + customer => customer.Name, + (id, name) => throw new InvalidOperationException("Domain factory rejected the values.")); + } + } + } +} diff --git a/test/Dapper.FluentMap.Tests/QueryMappedMultiMappingTests.cs b/test/Dapper.FluentMap.Tests/QueryMappedMultiMappingTests.cs index 8053635..1508839 100644 --- a/test/Dapper.FluentMap.Tests/QueryMappedMultiMappingTests.cs +++ b/test/Dapper.FluentMap.Tests/QueryMappedMultiMappingTests.cs @@ -484,6 +484,239 @@ public void QueryMappedShouldCloseOwnedConnectionWhenProjectionThrows() } } + [Fact] + [Trait("Category", "Integration")] + public void QueryMappedShouldMaterializeThreeSegmentsThroughSharedPipeline() + { + PreTest(typeof(JoinCustomer), typeof(JoinOrder), typeof(JoinShipment)); + try + { + FluentMapper.Initialize(configuration => + { + configuration.AddMap(new JoinCustomerMap()); + configuration.AddMap(new JoinOrderMap()); + configuration.AddMap(new JoinShipmentMap()); + }); + + using (var connection = OpenConnection()) + { + var row = connection.QueryMapped( + "SELECT 1 AS customer_id, 'Ada' AS customer_name, 10 AS order_id, 12.5 AS total, 100 AS shipment_id, 'sent' AS shipment_status;", + (customer, order, shipment) => new CustomerOrderShipment(customer, order, shipment), + splitOn: "order_id,shipment_id") + .Single(); + + Assert.Equal(1, row.Customer.Id); + Assert.Equal(10, row.Order.Id); + Assert.Equal(100, row.Shipment.Id); + Assert.Equal("sent", row.Shipment.Status); + } + } + finally + { + PreTest(typeof(JoinCustomer), typeof(JoinOrder), typeof(JoinShipment)); + } + } + + [Fact] + [Trait("Category", "Integration")] + public async System.Threading.Tasks.Task QueryMappedAsyncShouldMaterializeThreeSegments() + { + PreTest(typeof(JoinCustomer), typeof(JoinOrder), typeof(JoinShipment)); + try + { + FluentMapper.Initialize(configuration => + { + configuration.AddMap(new JoinCustomerMap()); + configuration.AddMap(new JoinOrderMap()); + configuration.AddMap(new JoinShipmentMap()); + }); + + using (var connection = OpenConnection()) + { + var rows = await connection.QueryMappedAsync( + "SELECT 2 AS customer_id, 'Grace' AS customer_name, 20 AS order_id, 25.5 AS total, 200 AS shipment_id, 'ready' AS shipment_status;", + (customer, order, shipment) => new CustomerOrderShipment(customer, order, shipment), + splitOn: "order_id,shipment_id", + cancellationToken: TestContext.Current.CancellationToken); + var row = rows.Single(); + + Assert.Equal(2, row.Customer.Id); + Assert.Equal(20, row.Order.Id); + Assert.Equal(200, row.Shipment.Id); + } + } + finally + { + PreTest(typeof(JoinCustomer), typeof(JoinOrder), typeof(JoinShipment)); + } + } + + [Fact] + [Trait("Category", "Integration")] + public void ThreeSegmentQueryShouldPassAllNullChildrenAsNull() + { + PreTest(typeof(JoinCustomer), typeof(JoinOrder), typeof(JoinShipment)); + try + { + FluentMapper.Initialize(configuration => + { + configuration.AddMap(new JoinCustomerMap()); + configuration.AddMap(new JoinOrderMap()); + configuration.AddMap(new JoinShipmentMap()); + }); + + using (var connection = OpenConnection()) + { + var row = connection.QueryMapped( + "SELECT 3 AS customer_id, 'Null children' AS customer_name, NULL AS order_id, NULL AS total, NULL AS shipment_id, NULL AS shipment_status;", + (customer, order, shipment) => new CustomerOrderShipment(customer, order, shipment), + splitOn: "order_id,shipment_id") + .Single(); + + Assert.NotNull(row.Customer); + Assert.Null(row.Order); + Assert.Null(row.Shipment); + } + } + finally + { + PreTest(typeof(JoinCustomer), typeof(JoinOrder), typeof(JoinShipment)); + } + } + + [Theory] + [InlineData("order_id")] + [InlineData("order_id,")] + [InlineData("order_id,order_id")] + [Trait("Category", "Integration")] + public void ThreeSegmentQueryShouldValidateEverySplitBoundary(string splitOn) + { + using (var connection = OpenConnection()) + { + var exception = Assert.ThrowsAny(() => + connection.QueryMapped( + "SELECT 1 AS customer_id, 2 AS order_id, 3 AS shipment_id;", + (customer, order, shipment) => new CustomerOrderShipment(customer, order, shipment), + splitOn: splitOn) + .ToList()); + + Assert.Contains("splitOn", exception.Message, StringComparison.OrdinalIgnoreCase); + } + } + + [Fact] + [Trait("Category", "Integration")] + public void ThreeSegmentQueryShouldHonorPerSegmentProfilesAndIsolatedRuntime() + { + var runtime = new FluentMapConfigurationBuilder() + .AddProfile() + .AddProfile() + .AddProfile() + .Build() + .CreateRuntime(); + + using (var connection = OpenConnection()) + { + var row = runtime.QueryMapped< + JoinCustomer, + JoinOrder, + JoinShipment, + CustomerOrderShipment, + LegacyProfile, + ArchiveProfile, + TrackingProfile>( + connection, + "SELECT 4 AS legacy_customer_id, 'Profiled' AS legal_name, 40 AS archived_order_id, 44.5 AS archived_total, 400 AS tracked_shipment_id, 'tracked' AS tracked_status;", + (customer, order, shipment) => new CustomerOrderShipment(customer, order, shipment), + splitOn: "archived_order_id,tracked_shipment_id") + .Single(); + + Assert.Equal(4, row.Customer.Id); + Assert.Equal(40, row.Order.Id); + Assert.Equal(400, row.Shipment.Id); + Assert.Equal("tracked", row.Shipment.Status); + } + } + + [Fact] + [Trait("Category", "Integration")] + public void ThreeSegmentGeneratedAndRuntimeMaterializationShouldBeEquivalent() + { + var generatedRuntime = CreateThreeSegmentRuntime(registerGenerated: true); + var reflectionRuntime = CreateThreeSegmentRuntime(registerGenerated: false); + + using (var connection = OpenConnection()) + { + const string sql = "SELECT 5 AS customer_id, 'Equivalent' AS customer_name, 50 AS order_id, 50.5 AS total, 500 AS shipment_id, 'equivalent' AS shipment_status;"; + var generated = generatedRuntime.QueryMapped( + connection, + sql, + (customer, order, shipment) => new CustomerOrderShipment(customer, order, shipment), + splitOn: "order_id,shipment_id").Single(); + var reflection = reflectionRuntime.QueryMapped( + connection, + sql, + (customer, order, shipment) => new CustomerOrderShipment(customer, order, shipment), + splitOn: "order_id,shipment_id").Single(); + + Assert.Equal(reflection.Customer.Id, generated.Customer.Id); + Assert.Equal(reflection.Customer.Name, generated.Customer.Name); + Assert.Equal(reflection.Order.Id, generated.Order.Id); + Assert.Equal(reflection.Order.Total, generated.Order.Total); + Assert.Equal(reflection.Shipment.Id, generated.Shipment.Id); + Assert.Equal(reflection.Shipment.Status, generated.Shipment.Status); + } + } + + private static FluentMapRuntime CreateThreeSegmentRuntime(bool registerGenerated) + { + var builder = new FluentMapConfigurationBuilder() + .AddMap(new JoinCustomerMap()) + .AddMap(new JoinOrderMap()) + .AddMap(new JoinShipmentMap()); + + if (registerGenerated) + { + builder + .AddGeneratedMaterializer( + new[] + { + GeneratedMaterializerColumn.Map("customer_id", nameof(JoinCustomer.Id)), + GeneratedMaterializerColumn.Map("customer_name", nameof(JoinCustomer.Name)) + }, + record => new JoinCustomer + { + Id = Convert.ToInt32(record.GetValue(0)), + Name = Convert.ToString(record.GetValue(1)) + }) + .AddGeneratedMaterializer( + new[] + { + GeneratedMaterializerColumn.Map("order_id", nameof(JoinOrder.Id)), + GeneratedMaterializerColumn.Map("total", nameof(JoinOrder.Total)) + }, + record => new JoinOrder + { + Id = Convert.ToInt32(record.GetValue(0)), + Total = Convert.ToDecimal(record.GetValue(1)) + }) + .AddGeneratedMaterializer( + new[] + { + GeneratedMaterializerColumn.Map("shipment_id", nameof(JoinShipment.Id)), + GeneratedMaterializerColumn.Map("shipment_status", nameof(JoinShipment.Status)) + }, + record => new JoinShipment + { + Id = Convert.ToInt32(record.GetValue(0)), + Status = Convert.ToString(record.GetValue(1)) + }); + } + + return builder.Build().CreateRuntime(); + } + private static CustomerOrder MaterializeCustomerOrder(bool registerGeneratedMaterializers) { PreTest(typeof(JoinCustomer), typeof(JoinOrder)); @@ -565,6 +798,22 @@ public CustomerOrder(JoinCustomer customer, JoinOrder order) public JoinOrder Order { get; } } + private sealed class CustomerOrderShipment + { + public CustomerOrderShipment(JoinCustomer customer, JoinOrder order, JoinShipment shipment) + { + Customer = customer; + Order = order; + Shipment = shipment; + } + + public JoinCustomer Customer { get; } + + public JoinOrder Order { get; } + + public JoinShipment Shipment { get; } + } + private sealed class JoinCustomer { public int Id { get; set; } @@ -579,6 +828,13 @@ private sealed class JoinOrder public decimal Total { get; set; } } + private sealed class JoinShipment + { + public int Id { get; set; } + + public string Status { get; set; } + } + private sealed class DefaultSplitOrder { public int Id { get; set; } @@ -604,6 +860,15 @@ public JoinOrderMap() } } + private sealed class JoinShipmentMap : EntityMap + { + public JoinShipmentMap() + { + Map(shipment => shipment.Id).ToColumn("shipment_id"); + Map(shipment => shipment.Status).ToColumn("shipment_status"); + } + } + private sealed class LegacyProfile : IMappingProfile { } @@ -612,6 +877,10 @@ private sealed class ArchiveProfile : IMappingProfile { } + private sealed class TrackingProfile : IMappingProfile + { + } + private sealed class LegacyJoinCustomerMap : EntityMap, IProfileMap { public LegacyJoinCustomerMap() @@ -630,6 +899,15 @@ public ArchivedJoinOrderMap() } } + private sealed class TrackedJoinShipmentMap : EntityMap, IProfileMap + { + public TrackedJoinShipmentMap() + { + Map(shipment => shipment.Id).ToColumn("tracked_shipment_id"); + Map(shipment => shipment.Status).ToColumn("tracked_status"); + } + } + private sealed class DefaultSplitOrderMap : EntityMap { public DefaultSplitOrderMap() From 23684c6415c051d004c122c9086703f774092982 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Wed, 30 Sep 2026 10:23:31 -0300 Subject: [PATCH 4/9] feat: [Roadmap] FluentMap 3.x: generated/AOT maturity, mapping completeness and API governance --- .editorconfig | 11 ++ .github/workflows/ci.yml | 33 ++++++ CHANGELOG.md | 3 + COMPATIBILITY.md | 6 + Directory.Build.props | 1 + MIGRATION.md | 3 + MIGRATION.pt-BR.md | 3 + README.md | 1 + README.pt-BR.md | 1 + .../Dapper.FluentMap.Benchmarks/Program.cs | 4 +- .../Dapper.FluentMap.Analyzers.csproj | 3 + ...apper.FluentMap.DependencyInjection.csproj | 2 + .../Dapper.FluentMap.Dommel.csproj | 3 + .../DommelWriteExtensions.cs | 14 +-- .../Dapper.FluentMap.Generators.csproj | 3 + .../ImmutableFluentMapConfiguration.cs | 2 +- src/Dapper.FluentMap/Dapper.FluentMap.csproj | 2 + .../Diagnostics/MappingExplanation.cs | 4 +- .../Diagnostics/MemberMappingExplanation.cs | 4 +- src/Dapper.FluentMap/FluentMapRuntime.cs | 94 +++++++-------- src/Dapper.FluentMap/GeneratedParameters.cs | 2 +- .../Mapping/PropertyConversionMetadata.cs | 4 +- .../GeneratedMaterializerColumn.cs | 8 +- .../GeneratedMaterializerDescriptor.cs | 2 +- src/Dapper.FluentMap/QueryMappedExtensions.cs | 110 +++++++++--------- ...uentMap.ProviderCompatibility.Tests.csproj | 12 +- 26 files changed, 207 insertions(+), 128 deletions(-) diff --git a/.editorconfig b/.editorconfig index af9e891..beeb708 100644 --- a/.editorconfig +++ b/.editorconfig @@ -140,3 +140,14 @@ dotnet_diagnostic.CA1859.severity = suggestion dotnet_diagnostic.CA1860.severity = suggestion dotnet_diagnostic.CA1861.severity = suggestion dotnet_diagnostic.CA1862.severity = suggestion + +# Checked-in API files are the approval record for shipped package surfaces. +dotnet_public_api_analyzer.require_api_files = true +dotnet_public_api_analyzer.enable_nullability = true +dotnet_diagnostic.RS0016.severity = error +dotnet_diagnostic.RS0017.severity = error +dotnet_diagnostic.RS0024.severity = error +dotnet_diagnostic.RS0025.severity = error +# Existing overload families intentionally retain their historical optional +# parameters; snapshots, rather than RS0026, govern future changes. +dotnet_diagnostic.RS0026.severity = none diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6162f37..c8984d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,6 +132,35 @@ jobs: run: >- dotnet ./test/Dapper.FluentMap.ProviderCompatibility.Tests/bin/Release/net10.0/Dapper.FluentMap.ProviderCompatibility.Tests.dll + public-api: + name: Public API and nullable contracts + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 1 + persist-credentials: false + show-progress: false + + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + global-json-file: global.json + + - name: Restore + run: dotnet restore ./Dapper.FluentMap.slnx + + - name: Validate checked-in API baselines and nullable annotations + run: | + dotnet build ./src/Dapper.FluentMap/Dapper.FluentMap.csproj --configuration Release --no-restore + dotnet build ./src/Dapper.FluentMap.Dommel/Dapper.FluentMap.Dommel.csproj --configuration Release --no-restore + dotnet build ./src/Dapper.FluentMap.DependencyInjection/Dapper.FluentMap.DependencyInjection.csproj --configuration Release --no-restore + dotnet build ./src/Dapper.FluentMap.Analyzers/Dapper.FluentMap.Analyzers.csproj --configuration Release --no-restore + dotnet build ./src/Dapper.FluentMap.Generators/Dapper.FluentMap.Generators.csproj --configuration Release --no-restore + roslyn-components: name: Analyzer and generator compatibility runs-on: ubuntu-latest @@ -467,6 +496,7 @@ jobs: needs: - repository-configuration - compatibility + - public-api - roslyn-components - provider-compatibility 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') }} @@ -773,6 +803,7 @@ jobs: needs: - repository-configuration - compatibility + - public-api - roslyn-components - provider-compatibility - native-aot @@ -785,6 +816,7 @@ jobs: run: | echo "repository-configuration=${{ needs.repository-configuration.result }}" echo "compatibility=${{ needs.compatibility.result }}" + echo "public-api=${{ needs.public-api.result }}" echo "roslyn-components=${{ needs.roslyn-components.result }}" echo "provider-compatibility=${{ needs.provider-compatibility.result }}" echo "native-aot=${{ needs.native-aot.result }}" @@ -793,6 +825,7 @@ jobs: if [[ "${{ needs.repository-configuration.result }}" != "success" || \ "${{ needs.compatibility.result }}" != "success" || \ + "${{ needs.public-api.result }}" != "success" || \ "${{ needs.provider-compatibility.result }}" != "success" || \ "${{ needs.native-aot.result }}" != "success" || \ "${{ needs.roslyn-components.result }}" != "success" || \ diff --git a/CHANGELOG.md b/CHANGELOG.md index 63a1b5c..dba14ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,14 @@ The historical archived package history is not reconstructed here. This changelo - Added three-input FluentMap-controlled multi-mapping with multiple validated `splitOn` boundaries, per-segment profiles, async execution, null-child semantics and isolated-runtime APIs. - Added explicit `ConstructUsing(...)` factory strategies for one to four mapped values, with configuration validation and deterministic generated/strict boundaries. - Added analyzer diagnostic `DFM016` for duplicate explicit construction strategies. +- Added checked-in nullable public API baselines for every shipped assembly, enforced by CI. +- Added report-only scheduled/manual compatibility canaries and materialization benchmark regression reporting with machine-readable artifacts. ### Changed - Generated materializers now accept safe additional unmapped reader columns and provide specific strict diagnostics for missing, duplicate/ambiguous and mapped additional columns. - Two- and three-input multi-mapping now share a single segment materialization pipeline. +- Public projects now emit deliberate nullable reference annotations using staged `Nullable=annotations`; optional query inputs, metadata and multi-mapping null-child delegates reflect actual behavior without changing CLR signatures. ## [3.4.0] - 2026-09-27 diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index ca739ee..32ef73c 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -69,6 +69,10 @@ Provider support is split into certification levels: Provider certification requires real integration tests against that provider and database. A Dommel SQL builder being registered is not the same as provider certification. +## Experimental Compatibility Canaries + +The scheduled/manual `Compatibility Canary` workflow checks the supported Dapper minimum/current lanes, resolves a newer Dapper stable or prerelease when one exists, and compiles/runs SQLite evidence against the latest resolvable stable provider clients. These lanes are early-warning experiments only: they neither expand nor reduce the certified matrix above. A canary result becomes a support claim only after the pinned required matrix, documentation and package contract are intentionally updated and reviewed. + ## AOT And Trimming Current status: @@ -126,6 +130,8 @@ The fork also adds public APIs for profiles, naming policies, generated material 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. +Checked-in `PublicAPI.Shipped.txt`/`PublicAPI.Unshipped.txt` files govern every shipped assembly and include nullable annotations. See [MAINTAINING.md](MAINTAINING.md) for the approval and release process. + ## Unsupported Environments Or Claims - Dapper major versions outside `[2.1.79,3.0.0)` are not currently supported. diff --git a/Directory.Build.props b/Directory.Build.props index dad4dd0..b02aa8d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,6 +7,7 @@ 2.1.89 [$(DapperMinimumSupportedVersion),3.0.0) [3.5.3,4.0.0) + 5.6.0 diff --git a/MIGRATION.md b/MIGRATION.md index f41e776..1a248c9 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -344,6 +344,9 @@ Three-input `QueryMapped` overloads now complemen ## Generated Registration +Public assemblies now carry nullable reference annotations. CLR signatures and binary compatibility are unchanged, but nullable-enabled consumers may receive more accurate compiler warnings: optional query parameters/transactions and optional metadata are nullable, and multi-mapping child arguments are nullable because an all-`NULL` segment is passed as `null`. Treat new warnings as contract guidance; do not disable nullable analysis globally to hide them. + + Install `FluentMap.Generators` and call: ```csharp diff --git a/MIGRATION.pt-BR.md b/MIGRATION.pt-BR.md index fb084d8..9e2ac33 100644 --- a/MIGRATION.pt-BR.md +++ b/MIGRATION.pt-BR.md @@ -344,6 +344,9 @@ Overloads `QueryMapped` com três entradas agora ## Registro Gerado +Os assemblies públicos agora carregam anotações de nullable reference types. As assinaturas CLR e a compatibilidade binária não mudam, mas consumidores com nullable habilitado podem receber warnings mais precisos: parâmetros/transações opcionais e metadados opcionais são nullable, e argumentos filhos de multi-mapping são nullable porque um segmento contendo apenas `NULL` é entregue como `null`. Trate os novos warnings como orientação de contrato; não desabilite a análise nullable globalmente para ocultá-los. + + Instale `FluentMap.Generators` e chame: ```csharp diff --git a/README.md b/README.md index 289bd73..64cc785 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ See [COMPATIBILITY.md](COMPATIBILITY.md) for: - [Compatibility](COMPATIBILITY.md) - [Changelog](CHANGELOG.md) - [Support](SUPPORT.md) +- [Maintainer governance](MAINTAINING.md) - [Português (Brasil)](README.pt-BR.md) ## Contributing diff --git a/README.pt-BR.md b/README.pt-BR.md index 733466a..79fee34 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -158,6 +158,7 @@ Consulte [COMPATIBILITY.md](COMPATIBILITY.md) para: - [Compatibilidade](COMPATIBILITY.md) - [Changelog](CHANGELOG.md) - [Suporte](SUPPORT.md) +- [Governança para mantenedores](MAINTAINING.md) - [English](README.md) ## Contribuição diff --git a/benchmarks/Dapper.FluentMap.Benchmarks/Program.cs b/benchmarks/Dapper.FluentMap.Benchmarks/Program.cs index acd0c94..85bedce 100644 --- a/benchmarks/Dapper.FluentMap.Benchmarks/Program.cs +++ b/benchmarks/Dapper.FluentMap.Benchmarks/Program.cs @@ -358,7 +358,7 @@ public int QueryMappedTwoSegments() { return _connection.QueryMapped( "SELECT Id AS customer_id, Name AS customer_name, Id AS order_id, Balance AS order_total FROM BenchmarkRows;", - (customer, order) => customer.Id + order.Id, + (customer, order) => customer.Id + (order?.Id ?? 0), splitOn: "order_id") .Count(); } @@ -368,7 +368,7 @@ public int QueryMappedThreeSegments() { return _connection.QueryMapped( "SELECT Id AS customer_id, Name AS customer_name, Id AS order_id, Balance AS order_total, Id AS shipment_id, City AS shipment_status FROM BenchmarkRows;", - (customer, order, shipment) => customer.Id + order.Id + shipment.Id, + (customer, order, shipment) => customer.Id + (order?.Id ?? 0) + (shipment?.Id ?? 0), splitOn: "order_id,shipment_id") .Count(); } diff --git a/src/Dapper.FluentMap.Analyzers/Dapper.FluentMap.Analyzers.csproj b/src/Dapper.FluentMap.Analyzers/Dapper.FluentMap.Analyzers.csproj index 0b53ca6..55396e2 100644 --- a/src/Dapper.FluentMap.Analyzers/Dapper.FluentMap.Analyzers.csproj +++ b/src/Dapper.FluentMap.Analyzers/Dapper.FluentMap.Analyzers.csproj @@ -4,6 +4,8 @@ Roslyn analyzers for Dapper.FluentMap that report statically detectable mapping and configuration problems during builds. $(FluentMapPackageVersionPrefix) netstandard2.0 + 8.0 + annotations true false FluentMap.Analyzers @@ -14,6 +16,7 @@ + diff --git a/src/Dapper.FluentMap.DependencyInjection/Dapper.FluentMap.DependencyInjection.csproj b/src/Dapper.FluentMap.DependencyInjection/Dapper.FluentMap.DependencyInjection.csproj index 5d15d9d..84ab26e 100644 --- a/src/Dapper.FluentMap.DependencyInjection/Dapper.FluentMap.DependencyInjection.csproj +++ b/src/Dapper.FluentMap.DependencyInjection/Dapper.FluentMap.DependencyInjection.csproj @@ -5,6 +5,7 @@ $(FluentMapPackageVersionPrefix) netstandard2.0 8.0 + annotations true FluentMap.DependencyInjection dapper;dependency-injection;di;fluent-mapping;mapping;database;fluentmap @@ -13,5 +14,6 @@ + diff --git a/src/Dapper.FluentMap.Dommel/Dapper.FluentMap.Dommel.csproj b/src/Dapper.FluentMap.Dommel/Dapper.FluentMap.Dommel.csproj index dc924d5..39a8cb4 100644 --- a/src/Dapper.FluentMap.Dommel/Dapper.FluentMap.Dommel.csproj +++ b/src/Dapper.FluentMap.Dommel/Dapper.FluentMap.Dommel.csproj @@ -5,6 +5,8 @@ Dommel integration for Dapper.FluentMap that configures Dommel table, key, generated-column, and persistence metadata through fluent mappings. $(FluentMapPackageVersionPrefix) netstandard2.0 + 8.0 + annotations true dapper;dommel;fluent-mapping;mapping;crud;database;sql;fluentmap true @@ -14,5 +16,6 @@ + diff --git a/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs b/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs index 95047c8..8863464 100644 --- a/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs +++ b/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs @@ -29,10 +29,10 @@ public static class DommelWriteExtensions /// The entity to insert. /// Optional transaction. /// The scalar value returned by the provider-specific Dommel insert statement. - public static object InsertMapped( + public static object? InsertMapped( this IDbConnection connection, TEntity entity, - IDbTransaction transaction = null) + IDbTransaction? transaction = null) where TEntity : class { var command = CreateInsertCommand(connection, entity, transaction, CancellationToken.None); @@ -42,15 +42,15 @@ public static object InsertMapped( /// /// Asynchronously inserts an entity using Dommel mapping and persistence metadata, applying configured property write converters. /// - public static Task InsertMappedAsync( + public static async Task InsertMappedAsync( this IDbConnection connection, TEntity entity, - IDbTransaction transaction = null, + IDbTransaction? transaction = null, CancellationToken cancellationToken = default) where TEntity : class { var command = CreateInsertCommand(connection, entity, transaction, cancellationToken); - return connection.ExecuteScalarAsync(command); + return await connection.ExecuteScalarAsync(command).ConfigureAwait(false); } /// @@ -60,7 +60,7 @@ public static Task InsertMappedAsync( public static bool UpdateMapped( this IDbConnection connection, TEntity entity, - IDbTransaction transaction = null) + IDbTransaction? transaction = null) where TEntity : class { var command = CreateUpdateCommand(connection, entity, transaction, CancellationToken.None); @@ -74,7 +74,7 @@ public static bool UpdateMapped( public static async Task UpdateMappedAsync( this IDbConnection connection, TEntity entity, - IDbTransaction transaction = null, + IDbTransaction? transaction = null, CancellationToken cancellationToken = default) where TEntity : class { diff --git a/src/Dapper.FluentMap.Generators/Dapper.FluentMap.Generators.csproj b/src/Dapper.FluentMap.Generators/Dapper.FluentMap.Generators.csproj index 1d313a6..5f8f11f 100644 --- a/src/Dapper.FluentMap.Generators/Dapper.FluentMap.Generators.csproj +++ b/src/Dapper.FluentMap.Generators/Dapper.FluentMap.Generators.csproj @@ -4,6 +4,8 @@ Roslyn source generators for Dapper.FluentMap that generate supported map registration and materialization helpers at build time. $(FluentMapPackageVersionPrefix) netstandard2.0 + 8.0 + annotations true false FluentMap.Generators @@ -14,6 +16,7 @@ + diff --git a/src/Dapper.FluentMap/Configuration/ImmutableFluentMapConfiguration.cs b/src/Dapper.FluentMap/Configuration/ImmutableFluentMapConfiguration.cs index e08a806..fc28fdd 100644 --- a/src/Dapper.FluentMap/Configuration/ImmutableFluentMapConfiguration.cs +++ b/src/Dapper.FluentMap/Configuration/ImmutableFluentMapConfiguration.cs @@ -209,7 +209,7 @@ private ProfileMappingConfiguration( /// /// Gets the selected mapping profile type. /// - public Type ProfileType { get; } + public Type? ProfileType { get; } /// /// Gets the concrete map type that produced this profile configuration. diff --git a/src/Dapper.FluentMap/Dapper.FluentMap.csproj b/src/Dapper.FluentMap/Dapper.FluentMap.csproj index f6b0078..e8889d8 100644 --- a/src/Dapper.FluentMap/Dapper.FluentMap.csproj +++ b/src/Dapper.FluentMap/Dapper.FluentMap.csproj @@ -6,6 +6,7 @@ $(FluentMapPackageVersionPrefix) netstandard2.0 8.0 + annotations true dapper;fluent-mapping;mapping;micro-orm;database;sql;poco;fluentmap true @@ -14,5 +15,6 @@ + diff --git a/src/Dapper.FluentMap/Diagnostics/MappingExplanation.cs b/src/Dapper.FluentMap/Diagnostics/MappingExplanation.cs index cd34319..e6db64d 100644 --- a/src/Dapper.FluentMap/Diagnostics/MappingExplanation.cs +++ b/src/Dapper.FluentMap/Diagnostics/MappingExplanation.cs @@ -43,12 +43,12 @@ internal MappingExplanation( /// /// Gets the mapping profile marker type, when this explanation targets a profile. /// - public Type ProfileType { get; } + public Type? ProfileType { get; } /// /// Gets the registered entity map type, when one exists. /// - public Type EntityMapType { get; } + public Type? EntityMapType { get; } /// /// Gets the registered convention types for the entity. diff --git a/src/Dapper.FluentMap/Diagnostics/MemberMappingExplanation.cs b/src/Dapper.FluentMap/Diagnostics/MemberMappingExplanation.cs index cde7e20..cecf080 100644 --- a/src/Dapper.FluentMap/Diagnostics/MemberMappingExplanation.cs +++ b/src/Dapper.FluentMap/Diagnostics/MemberMappingExplanation.cs @@ -84,12 +84,12 @@ internal MemberMappingExplanation( /// /// Gets the base entity type that declared an inherited mapping, when applicable. /// - public Type InheritedFrom { get; } + public Type? InheritedFrom { get; } /// /// Gets the convention type that produced the mapping, when applicable. /// - public Type ConventionType { get; } + public Type? ConventionType { get; } /// /// Gets constructor parameters that can receive this mapped column. diff --git a/src/Dapper.FluentMap/FluentMapRuntime.cs b/src/Dapper.FluentMap/FluentMapRuntime.cs index de8248b..ae759ab 100644 --- a/src/Dapper.FluentMap/FluentMapRuntime.cs +++ b/src/Dapper.FluentMap/FluentMapRuntime.cs @@ -101,8 +101,8 @@ public IEnumerable QueryMapped< TEntity>( IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -130,8 +130,8 @@ public IEnumerable QueryMapped< TProfile>( IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -162,9 +162,9 @@ public IEnumerable QueryMapped< TReturn>( IDbConnection connection, string sql, - Func map, - object param = null, - IDbTransaction transaction = null, + Func map, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, string splitOn = "Id") @@ -202,12 +202,12 @@ public IEnumerable QueryMapped< TReturn>( IDbConnection connection, string sql, - Func map, - object param = null, - IDbTransaction transaction = null, + Func map, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, - string splitOn = null) + string? splitOn = null) where TFirst : class where TSecond : class where TThird : class @@ -240,12 +240,12 @@ public Task> QueryMappedAsync< TReturn>( IDbConnection connection, string sql, - Func map, - object param = null, - IDbTransaction transaction = null, + Func map, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, - string splitOn = null, + string? splitOn = null, CancellationToken cancellationToken = default) where TFirst : class where TSecond : class @@ -286,12 +286,12 @@ public Task> QueryMappedAsync< TThirdProfile>( IDbConnection connection, string sql, - Func map, - object param = null, - IDbTransaction transaction = null, + Func map, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, - string splitOn = null, + string? splitOn = null, CancellationToken cancellationToken = default) where TFirst : class where TSecond : class @@ -335,12 +335,12 @@ public IEnumerable QueryMapped< TThirdProfile>( IDbConnection connection, string sql, - Func map, - object param = null, - IDbTransaction transaction = null, + Func map, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, - string splitOn = null) + string? splitOn = null) where TFirst : class where TSecond : class where TThird : class @@ -379,9 +379,9 @@ public IEnumerable QueryMapped< TSecondProfile>( IDbConnection connection, string sql, - Func map, - object param = null, - IDbTransaction transaction = null, + Func map, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, string splitOn = "Id") @@ -415,8 +415,8 @@ public TEntity QueryMappedSingle< TEntity>( IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -435,8 +435,8 @@ public TEntity QueryMappedSingle< TProfile>( IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -463,8 +463,8 @@ public TEntity QueryMappedSingle< public IEnumerable QueryGeneratedMapped( IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -506,8 +506,8 @@ public IEnumerable QueryGeneratedMapped( public IEnumerable QueryGeneratedMapped( IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -537,8 +537,8 @@ public IEnumerable QueryGeneratedMapped( public TEntity QueryGeneratedMappedSingle( IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -552,8 +552,8 @@ public TEntity QueryGeneratedMappedSingle( public TEntity QueryGeneratedMappedSingle( IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -594,8 +594,8 @@ public IEnumerable QueryMappedUnbuffered< TEntity>( IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -623,8 +623,8 @@ public IEnumerable QueryMappedUnbuffered< TProfile>( IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -652,8 +652,8 @@ public IAsyncEnumerable QueryMappedUnbufferedAsync< TEntity>( DbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, CancellationToken cancellationToken = default) @@ -686,7 +686,7 @@ public IAsyncEnumerable QueryMappedUnbufferedAsync< /// [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] - public MappedGridReader QueryMultipleMapped(IDbConnection connection, string sql, object param = null) + public MappedGridReader QueryMultipleMapped(IDbConnection connection, string sql, object? param = null) { if (connection == null) { @@ -711,8 +711,8 @@ public MappedGridReader QueryMultipleMapped(IDbConnection connection, string sql public Task QueryMultipleMappedAsync( DbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, CancellationToken cancellationToken = default) diff --git a/src/Dapper.FluentMap/GeneratedParameters.cs b/src/Dapper.FluentMap/GeneratedParameters.cs index e0022a9..cccafe9 100644 --- a/src/Dapper.FluentMap/GeneratedParameters.cs +++ b/src/Dapper.FluentMap/GeneratedParameters.cs @@ -26,7 +26,7 @@ public sealed class GeneratedParameters /// The optional provider parameter size. /// This parameter collection. /// Thrown when is empty or duplicated, or is negative. - public GeneratedParameters Add(string name, object value, DbType dbType, int? size = null) + public GeneratedParameters Add(string name, object? value, DbType dbType, int? size = null) { if (string.IsNullOrWhiteSpace(name)) { diff --git a/src/Dapper.FluentMap/Mapping/PropertyConversionMetadata.cs b/src/Dapper.FluentMap/Mapping/PropertyConversionMetadata.cs index 7aea6bf..59496a1 100644 --- a/src/Dapper.FluentMap/Mapping/PropertyConversionMetadata.cs +++ b/src/Dapper.FluentMap/Mapping/PropertyConversionMetadata.cs @@ -162,12 +162,12 @@ private PropertyConversionMetadata( /// /// Gets the read converter descriptor, or when no read converter is configured. /// - public PropertyConverterMetadata ReadConverter { get; } + public PropertyConverterMetadata? ReadConverter { get; } /// /// Gets the write converter descriptor, or when no write converter is configured. /// - public PropertyConverterMetadata WriteConverter { get; } + public PropertyConverterMetadata? WriteConverter { get; } internal PropertyConversionMetadata WithReadConverter(PropertyConverterMetadata converter) { diff --git a/src/Dapper.FluentMap/Materialization/GeneratedMaterializerColumn.cs b/src/Dapper.FluentMap/Materialization/GeneratedMaterializerColumn.cs index be97ea6..5610443 100644 --- a/src/Dapper.FluentMap/Materialization/GeneratedMaterializerColumn.cs +++ b/src/Dapper.FluentMap/Materialization/GeneratedMaterializerColumn.cs @@ -41,7 +41,7 @@ private GeneratedMaterializerColumn( /// /// Gets the mapped member path expected for this column, or for ignored columns. /// - public string MemberPath { get; } + public string? MemberPath { get; } /// /// Gets a value indicating whether the current mapping must ignore this column. @@ -51,17 +51,17 @@ private GeneratedMaterializerColumn( /// /// Gets the read converter type applied by the generated materializer, or . /// - public Type ReadConverterType { get; } + public Type? ReadConverterType { get; } /// /// Gets the database/provider CLR type accepted by the generated read converter, or . /// - public Type ReadConverterDatabaseType { get; } + public Type? ReadConverterDatabaseType { get; } /// /// Gets the property CLR type returned by the generated read converter, or . /// - public Type ReadConverterPropertyType { get; } + public Type? ReadConverterPropertyType { get; } /// /// Creates a descriptor for a materialized column. diff --git a/src/Dapper.FluentMap/Materialization/GeneratedMaterializerDescriptor.cs b/src/Dapper.FluentMap/Materialization/GeneratedMaterializerDescriptor.cs index f4928e2..5b9568d 100644 --- a/src/Dapper.FluentMap/Materialization/GeneratedMaterializerDescriptor.cs +++ b/src/Dapper.FluentMap/Materialization/GeneratedMaterializerDescriptor.cs @@ -75,7 +75,7 @@ public GeneratedMaterializerDescriptor( /// /// Gets the mapping profile type, or for the default map. /// - public Type ProfileType { get; } + public Type? ProfileType { get; } /// /// Gets the ordered column shape and member bindings expected by the materializer. diff --git a/src/Dapper.FluentMap/QueryMappedExtensions.cs b/src/Dapper.FluentMap/QueryMappedExtensions.cs index c87dadb..855f7ab 100644 --- a/src/Dapper.FluentMap/QueryMappedExtensions.cs +++ b/src/Dapper.FluentMap/QueryMappedExtensions.cs @@ -35,8 +35,8 @@ public static IEnumerable QueryMapped< TEntity>( this IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -76,8 +76,8 @@ public static IEnumerable QueryMapped< TProfile>( this IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -152,8 +152,8 @@ public static IEnumerable QueryMappedUnbuffered< TEntity>( this IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -193,8 +193,8 @@ public static IEnumerable QueryMappedUnbuffered< TProfile>( this IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -303,8 +303,8 @@ public static IAsyncEnumerable QueryMappedUnbufferedAsync< TEntity>( this DbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, CancellationToken cancellationToken = default) @@ -377,8 +377,8 @@ public static IAsyncEnumerable QueryMappedUnbufferedAsync< TProfile>( this DbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, CancellationToken cancellationToken = default) @@ -469,8 +469,8 @@ public static TEntity QueryMappedSingle< TEntity>( this IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -498,8 +498,8 @@ public static TEntity QueryMappedSingle< TProfile>( this IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -528,8 +528,8 @@ public static Task> QueryMappedAsync< TProfile>( this IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -587,8 +587,8 @@ public static async Task QueryMappedSingleAsync< TProfile>( this IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) where TEntity : class @@ -630,9 +630,9 @@ public static IEnumerable QueryMapped< TReturn>( this IDbConnection connection, string sql, - Func map, - object param = null, - IDbTransaction transaction = null, + Func map, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, string splitOn = "Id") @@ -664,7 +664,7 @@ public static IEnumerable QueryMapped< TReturn>( this IDbConnection connection, CommandDefinition command, - Func map, + Func map, string splitOn = "Id") where TFirst : class where TSecond : class @@ -696,12 +696,12 @@ public static IEnumerable QueryMapped< TReturn>( this IDbConnection connection, string sql, - Func map, - object param = null, - IDbTransaction transaction = null, + Func map, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, - string splitOn = null) + string? splitOn = null) where TFirst : class where TSecond : class where TThird : class @@ -730,8 +730,8 @@ public static IEnumerable QueryMapped< TReturn>( this IDbConnection connection, CommandDefinition command, - Func map, - string splitOn = null) + Func map, + string? splitOn = null) where TFirst : class where TSecond : class where TThird : class @@ -755,12 +755,12 @@ public static IEnumerable QueryMapped< TThirdProfile>( this IDbConnection connection, string sql, - Func map, - object param = null, - IDbTransaction transaction = null, + Func map, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, - string splitOn = null) + string? splitOn = null) where TFirst : class where TSecond : class where TThird : class @@ -799,7 +799,7 @@ public static IEnumerable QueryMapped< TThirdProfile>( this IDbConnection connection, CommandDefinition command, - Func map, + Func map, string splitOn) where TFirst : class where TSecond : class @@ -831,12 +831,12 @@ public static Task> QueryMappedAsync< TReturn>( this IDbConnection connection, string sql, - Func map, - object param = null, - IDbTransaction transaction = null, + Func map, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, - string splitOn = null, + string? splitOn = null, CancellationToken cancellationToken = default) where TFirst : class where TSecond : class @@ -874,7 +874,7 @@ public static Task> QueryMappedAsync< TReturn>( this IDbConnection connection, CommandDefinition command, - Func map, + Func map, string splitOn) where TFirst : class where TSecond : class @@ -910,12 +910,12 @@ public static Task> QueryMappedAsync< TThirdProfile>( this IDbConnection connection, string sql, - Func map, - object param = null, - IDbTransaction transaction = null, + Func map, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, - string splitOn = null, + string? splitOn = null, CancellationToken cancellationToken = default) where TFirst : class where TSecond : class @@ -959,7 +959,7 @@ public static Task> QueryMappedAsync< TThirdProfile>( this IDbConnection connection, CommandDefinition command, - Func map, + Func map, string splitOn) where TFirst : class where TSecond : class @@ -1012,9 +1012,9 @@ public static IEnumerable QueryMapped< TSecondProfile>( this IDbConnection connection, string sql, - Func map, - object param = null, - IDbTransaction transaction = null, + Func map, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, string splitOn = "Id") @@ -1050,7 +1050,7 @@ public static IEnumerable QueryMapped< TSecondProfile>( this IDbConnection connection, CommandDefinition command, - Func map, + Func map, string splitOn = "Id") where TFirst : class where TSecond : class @@ -1082,8 +1082,8 @@ public static IEnumerable QueryMapped< public static MappedGridReader QueryMultipleMapped( this IDbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { @@ -1125,8 +1125,8 @@ public static MappedGridReader QueryMultipleMapped( public static Task QueryMultipleMappedAsync( this DbConnection connection, string sql, - object param = null, - IDbTransaction transaction = null, + object? param = null, + IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, CancellationToken cancellationToken = default) @@ -1254,7 +1254,7 @@ internal static IEnumerable ExecuteMapped< TReturn>( IDbConnection connection, CommandDefinition command, - Func map, + Func map, string splitOn, Type firstProfileType, Type secondProfileType, @@ -1284,7 +1284,7 @@ internal static IEnumerable ExecuteMapped< TReturn>( IDbConnection connection, CommandDefinition command, - Func map, + Func map, string splitOn, Type firstProfileType, Type secondProfileType, diff --git a/test/Dapper.FluentMap.ProviderCompatibility.Tests/Dapper.FluentMap.ProviderCompatibility.Tests.csproj b/test/Dapper.FluentMap.ProviderCompatibility.Tests/Dapper.FluentMap.ProviderCompatibility.Tests.csproj index 806619e..47ccc86 100644 --- a/test/Dapper.FluentMap.ProviderCompatibility.Tests/Dapper.FluentMap.ProviderCompatibility.Tests.csproj +++ b/test/Dapper.FluentMap.ProviderCompatibility.Tests/Dapper.FluentMap.ProviderCompatibility.Tests.csproj @@ -2,14 +2,18 @@ net10.0 false + 7.1.0 + 10.0.12 + 2.6.2 + 10.0.3 - - - - + + + + From 0a0f5e9c52362cea6554fc86d577f440312914aa Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Wed, 30 Sep 2026 13:51:22 -0300 Subject: [PATCH 5/9] chore: complete API governance and regression evidence (#215) --- .github/workflows/compatibility-canary.yml | 152 +++++++ .github/workflows/performance-report.yml | 73 ++++ MAINTAINING.md | 39 ++ .../materialization-windows-net10.json | 18 + eng/compare-benchmark-results.ps1 | 76 ++++ .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 16 + .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 3 + .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 39 ++ .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 10 + .../GeneratedMaterializerColumn.cs | 8 +- .../GeneratedMaterializerDescriptor.cs | 2 +- src/Dapper.FluentMap/PublicAPI.Shipped.txt | 1 + src/Dapper.FluentMap/PublicAPI.Unshipped.txt | 380 ++++++++++++++++++ 17 files changed, 816 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/compatibility-canary.yml create mode 100644 .github/workflows/performance-report.yml create mode 100644 MAINTAINING.md create mode 100644 benchmarks/baselines/materialization-windows-net10.json create mode 100644 eng/compare-benchmark-results.ps1 create mode 100644 src/Dapper.FluentMap.Analyzers/PublicAPI.Shipped.txt create mode 100644 src/Dapper.FluentMap.Analyzers/PublicAPI.Unshipped.txt create mode 100644 src/Dapper.FluentMap.DependencyInjection/PublicAPI.Shipped.txt create mode 100644 src/Dapper.FluentMap.DependencyInjection/PublicAPI.Unshipped.txt create mode 100644 src/Dapper.FluentMap.Dommel/PublicAPI.Shipped.txt create mode 100644 src/Dapper.FluentMap.Dommel/PublicAPI.Unshipped.txt create mode 100644 src/Dapper.FluentMap.Generators/PublicAPI.Shipped.txt create mode 100644 src/Dapper.FluentMap.Generators/PublicAPI.Unshipped.txt create mode 100644 src/Dapper.FluentMap/PublicAPI.Shipped.txt create mode 100644 src/Dapper.FluentMap/PublicAPI.Unshipped.txt diff --git a/.github/workflows/compatibility-canary.yml b/.github/workflows/compatibility-canary.yml new file mode 100644 index 0000000..5e7c20a --- /dev/null +++ b/.github/workflows/compatibility-canary.yml @@ -0,0 +1,152 @@ +name: Compatibility Canary + +on: + schedule: + - cron: "43 7 * * 2" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: compatibility-canary-${{ github.repository }} + cancel-in-progress: false + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: "true" + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "true" + +jobs: + supported-dapper: + name: Supported evidence (${{ matrix.label }}, Dapper ${{ matrix.version }}) + runs-on: ubuntu-latest + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + include: + - label: minimum-supported + version: 2.1.79 + - label: latest-supported + version: 2.1.89 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + global-json-file: global.json + + - name: Restore supported lane + run: dotnet restore ./Dapper.FluentMap.slnx -p:DapperPackageVersion=${{ matrix.version }} + + - name: Build supported lane + run: dotnet build ./Dapper.FluentMap.slnx --configuration Release --no-restore -p:DapperPackageVersion=${{ matrix.version }} + + - name: Test supported lane + run: >- + dotnet test ./test/Dapper.FluentMap.Tests/Dapper.FluentMap.Tests.csproj + --configuration Release --no-build + + forward-dapper: + name: Experimental forward Dapper + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + global-json-file: global.json + + - name: Resolve forward-looking Dapper version + id: resolve + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $supported = '2.1.89' + $index = Invoke-RestMethod 'https://api.nuget.org/v3-flatcontainer/dapper/index.json' + $candidates = @( + $index.versions | + Where-Object { [version](($_ -split '-', 2)[0]) -gt [version]$supported } | + Sort-Object { [version](($_ -split '-', 2)[0]) } + ) + $candidate = if ($candidates.Count -eq 0) { $null } else { $candidates[-1] } + $available = -not [string]::IsNullOrWhiteSpace($candidate) + "available=$($available.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT + "version=$candidate" >> $env:GITHUB_OUTPUT + if ($available) { + "### Experimental Dapper candidate: $candidate" >> $env:GITHUB_STEP_SUMMARY + } else { + '### No newer Dapper stable/prerelease is currently resolvable.' >> $env:GITHUB_STEP_SUMMARY + } + + - name: Restore experimental lane + if: ${{ steps.resolve.outputs.available == 'true' }} + run: dotnet restore ./Dapper.FluentMap.slnx -p:DapperPackageVersion=${{ steps.resolve.outputs.version }} + + - name: Build experimental lane + if: ${{ steps.resolve.outputs.available == 'true' }} + run: dotnet build ./Dapper.FluentMap.slnx --configuration Release --no-restore -p:DapperPackageVersion=${{ steps.resolve.outputs.version }} + + - name: Test experimental lane + if: ${{ steps.resolve.outputs.available == 'true' }} + run: >- + dotnet test ./test/Dapper.FluentMap.Tests/Dapper.FluentMap.Tests.csproj + --configuration Release --no-build + + provider-clients: + name: Experimental latest provider clients + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + global-json-file: global.json + + - name: Restore latest stable provider clients + run: >- + dotnet restore ./test/Dapper.FluentMap.ProviderCompatibility.Tests/Dapper.FluentMap.ProviderCompatibility.Tests.csproj + -p:SqlClientPackageVersion=* + -p:SqliteClientPackageVersion=* + -p:MySqlConnectorPackageVersion=* + -p:NpgsqlPackageVersion=* + + - name: Report resolved experimental clients + shell: pwsh + run: | + $assets = Get-Content -Raw './test/Dapper.FluentMap.ProviderCompatibility.Tests/obj/project.assets.json' | ConvertFrom-Json + $packages = @('Microsoft.Data.SqlClient/', 'Microsoft.Data.Sqlite/', 'MySqlConnector/', 'Npgsql/') + foreach ($prefix in $packages) { + $resolved = @($assets.libraries.PSObject.Properties.Name | Where-Object { $_ -like "$prefix*" }) + "- experimental: $($resolved -join ', ')" >> $env:GITHUB_STEP_SUMMARY + } + + - name: Build against experimental clients + run: >- + dotnet build ./test/Dapper.FluentMap.ProviderCompatibility.Tests/Dapper.FluentMap.ProviderCompatibility.Tests.csproj + --configuration Release --no-restore + -p:SqlClientPackageVersion=* + -p:SqliteClientPackageVersion=* + -p:MySqlConnectorPackageVersion=* + -p:NpgsqlPackageVersion=* + + - name: Run always-available SQLite evidence + env: + DFM_PROVIDER_FILTER: SQLite + run: >- + dotnet ./test/Dapper.FluentMap.ProviderCompatibility.Tests/bin/Release/net10.0/Dapper.FluentMap.ProviderCompatibility.Tests.dll + -failSkips -parallelMode none diff --git a/.github/workflows/performance-report.yml b/.github/workflows/performance-report.yml new file mode 100644 index 0000000..3df8e8d --- /dev/null +++ b/.github/workflows/performance-report.yml @@ -0,0 +1,73 @@ +name: Performance Regression Report + +on: + schedule: + - cron: "19 9 * * 3" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: performance-report-${{ github.repository }} + cancel-in-progress: false + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: "true" + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "true" + +jobs: + materialization: + name: Materialization hot paths (report only) + runs-on: windows-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + global-json-file: global.json + + - name: Restore benchmark project + run: dotnet restore ./benchmarks/Dapper.FluentMap.Benchmarks/Dapper.FluentMap.Benchmarks.csproj + + - name: Build benchmark project + run: dotnet build ./benchmarks/Dapper.FluentMap.Benchmarks/Dapper.FluentMap.Benchmarks.csproj --configuration Release --no-restore + + - name: Run materialization benchmark report + run: >- + dotnet run --project ./benchmarks/Dapper.FluentMap.Benchmarks/Dapper.FluentMap.Benchmarks.csproj + --configuration Release --no-build -- + --filter "*DapperWithFluentMapRootMapping*" "*QueryMappedSimple" "*RuntimeQueryMappedSimple" + "*RuntimeQueryGeneratedSimpleExtraColumn*" "*QueryMappedGeneratedPropertyConverter*" + "*QueryMappedTwoSegments*" "*QueryMappedThreeSegments*" + --exporters json + + - name: Compare with checked-in baseline + shell: pwsh + run: | + $result = Get-ChildItem './.tmp/benchmarks/BenchmarkDotNet.Artifacts/results/*-report-full-compressed.json' | + Select-Object -First 1 + if ($null -eq $result) { + throw 'BenchmarkDotNet JSON output was not produced.' + } + ./eng/compare-benchmark-results.ps1 ` + -BaselinePath './benchmarks/baselines/materialization-windows-net10.json' ` + -CurrentPath $result.FullName ` + -OutputDirectory './artifacts/benchmark-report' + Get-Content './artifacts/benchmark-report/benchmark-comparison.md' >> $env:GITHUB_STEP_SUMMARY + + - name: Upload benchmark evidence + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: materialization-benchmark-report + path: | + ./.tmp/benchmarks/BenchmarkDotNet.Artifacts/results/* + ./artifacts/benchmark-report/* + if-no-files-found: error + retention-days: 90 diff --git a/MAINTAINING.md b/MAINTAINING.md new file mode 100644 index 0000000..4b9c665 --- /dev/null +++ b/MAINTAINING.md @@ -0,0 +1,39 @@ +# Maintainer Governance + +This document defines the review process for public API, nullable contracts, compatibility canaries and performance evidence. These checks supplement package validation, provider certification, security analysis and the normal test suite. + +## Public API baselines + +Every shipped project under `src/` has `PublicAPI.Shipped.txt` and `PublicAPI.Unshipped.txt`. `Microsoft.CodeAnalysis.PublicApiAnalyzers` validates their union during every build, including the dedicated `Public API and nullable contracts` CI job. + +For an intentional API change: + +1. update implementation, XML documentation and focused tests; +2. run `dotnet format analyzers --diagnostics RS0016 RS0017` to update `PublicAPI.Unshipped.txt`; +3. review the API-file diff as a compatibility contract, including nullability markers; +4. document consumer and SemVer impact; +5. after the API ships, move its entries from `PublicAPI.Unshipped.txt` to `PublicAPI.Shipped.txt` in ordinal order. + +Do not hand-approve a removal or signature replacement without the compatibility and SemVer review required for a public library. Package validation remains enabled and is not replaced by these files. + +## Nullable contracts + +Maintained public projects compile with C# nullable annotations enabled. Warning enforcement is staged as `Nullable=annotations` because enabling full flow analysis currently exposes substantial internal legacy debt; this prevents a blanket suppression migration. Public signatures must still model actual behavior deliberately. + +In particular, optional query parameters and transactions are nullable, ignored generated columns expose a nullable member path, optional profiles/converters are nullable, and multi-mapping delegates receive nullable child segments for all-`NULL` LEFT JOIN segments. Nullable annotation changes preserve CLR binary signatures but can create or remove consumer compiler warnings, so they require API-baseline review and changelog notes. + +## Compatibility canaries + +`Compatibility Canary` is scheduled and manually runnable. It records: + +- the minimum and latest supported Dapper lanes; +- a forward-looking Dapper stable/prerelease when NuGet exposes one newer than the certified lane; +- compilation and SQLite evidence against the latest resolvable stable provider clients. + +The canary is early-warning evidence. A green canary does not add a supported version, and a red canary does not remove one. Only the pinned required CI/provider matrix and `COMPATIBILITY.md` define certification. Investigate failures, record upstream incompatibilities and intentionally update the supported matrix only through a separately reviewed change. + +## Performance reporting + +`Performance Regression Report` runs the materialization hot paths on a scheduled/manual Windows hosted runner, exports BenchmarkDotNet CSV/JSON/Markdown/HTML, compares ShortRun results with `benchmarks/baselines/materialization-windows-net10.json`, and uploads both raw and comparison artifacts for 90 days. + +The initial signals are report-only: +35% mean time or +15% allocated bytes is highlighted for review. The time threshold reflects observed hosted-runner variance; allocation is more stable but still not a required PR gate. Reproduce a signal on controlled hardware before treating it as a regression. Update the checked-in baseline only after reviewing the code change, environment/runtime change, repeated measurements and generated comparison artifact. diff --git a/benchmarks/baselines/materialization-windows-net10.json b/benchmarks/baselines/materialization-windows-net10.json new file mode 100644 index 0000000..7da9e71 --- /dev/null +++ b/benchmarks/baselines/materialization-windows-net10.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": "1.0", + "runtime": ".NET 10", + "operatingSystem": "Windows", + "job": "ShortRun", + "timeRegressionThresholdPercent": 35, + "allocationRegressionThresholdPercent": 15, + "reportOnly": true, + "scenarios": [ + { "method": "DapperWithFluentMapRootMapping", "meanNanoseconds": 3066174, "allocatedBytes": 290096 }, + { "method": "QueryMappedSimple", "meanNanoseconds": 4179233, "allocatedBytes": 267784 }, + { "method": "RuntimeQueryMappedSimple", "meanNanoseconds": 3238907, "allocatedBytes": 267784 }, + { "method": "RuntimeQueryGeneratedSimpleExtraColumn", "meanNanoseconds": 3220158, "allocatedBytes": 304867 }, + { "method": "QueryMappedGeneratedPropertyConverter", "meanNanoseconds": 1207906, "allocatedBytes": 146608 }, + { "method": "QueryMappedTwoSegments", "meanNanoseconds": 1795108, "allocatedBytes": 269200 }, + { "method": "QueryMappedThreeSegments", "meanNanoseconds": 5463660, "allocatedBytes": 398976 } + ] +} diff --git a/eng/compare-benchmark-results.ps1 b/eng/compare-benchmark-results.ps1 new file mode 100644 index 0000000..be573b1 --- /dev/null +++ b/eng/compare-benchmark-results.ps1 @@ -0,0 +1,76 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$BaselinePath, + + [Parameter(Mandatory = $true)] + [string]$CurrentPath, + + [Parameter(Mandatory = $true)] + [string]$OutputDirectory +) + +$ErrorActionPreference = 'Stop' + +$baseline = Get-Content -Raw -LiteralPath $BaselinePath | ConvertFrom-Json +$current = Get-Content -Raw -LiteralPath $CurrentPath | ConvertFrom-Json +$shortRun = @($current.Benchmarks | Where-Object { $_.DisplayInfo -like '*ShortRun*' }) + +New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null + +$comparisons = foreach ($scenario in $baseline.scenarios) { + $matches = @($shortRun | Where-Object { $_.Method -eq $scenario.method }) + if ($matches.Count -ne 1) { + throw "Expected one ShortRun result for '$($scenario.method)', found $($matches.Count)." + } + + $result = $matches[0] + $timeDelta = (($result.Statistics.Mean - $scenario.meanNanoseconds) / $scenario.meanNanoseconds) * 100 + $allocationDelta = (($result.Memory.BytesAllocatedPerOperation - $scenario.allocatedBytes) / $scenario.allocatedBytes) * 100 + $material = $timeDelta -gt $baseline.timeRegressionThresholdPercent -or + $allocationDelta -gt $baseline.allocationRegressionThresholdPercent + + [pscustomobject]@{ + method = $scenario.method + baselineMeanNanoseconds = [math]::Round($scenario.meanNanoseconds, 2) + currentMeanNanoseconds = [math]::Round($result.Statistics.Mean, 2) + timeDeltaPercent = [math]::Round($timeDelta, 2) + baselineAllocatedBytes = [long]$scenario.allocatedBytes + currentAllocatedBytes = [long]$result.Memory.BytesAllocatedPerOperation + allocationDeltaPercent = [math]::Round($allocationDelta, 2) + materialRegression = $material + } +} + +$report = [pscustomobject]@{ + schemaVersion = '1.0' + reportOnly = [bool]$baseline.reportOnly + timeRegressionThresholdPercent = $baseline.timeRegressionThresholdPercent + allocationRegressionThresholdPercent = $baseline.allocationRegressionThresholdPercent + generatedAtUtc = [DateTime]::UtcNow.ToString('O') + comparisons = @($comparisons) +} + +$jsonPath = Join-Path $OutputDirectory 'benchmark-comparison.json' +$markdownPath = Join-Path $OutputDirectory 'benchmark-comparison.md' +$report | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $jsonPath -Encoding utf8 + +$lines = @( + '# Materialization benchmark comparison', + '', + "Report-only thresholds: time +$($baseline.timeRegressionThresholdPercent)%; allocation +$($baseline.allocationRegressionThresholdPercent)%.", + '', + '| Scenario | Time delta | Allocation delta | Signal |', + '| --- | ---: | ---: | --- |' +) + +foreach ($comparison in $comparisons) { + $signal = if ($comparison.materialRegression) { 'Review' } else { 'Within threshold' } + $lines += "| $($comparison.method) | $($comparison.timeDeltaPercent)% | $($comparison.allocationDeltaPercent)% | $signal |" +} + +$lines += '' +$lines += 'Hosted-runner measurements are evidence for review and do not fail the workflow on threshold crossings.' +$lines | Set-Content -LiteralPath $markdownPath -Encoding utf8 + +Get-Content -LiteralPath $markdownPath diff --git a/src/Dapper.FluentMap.Analyzers/PublicAPI.Shipped.txt b/src/Dapper.FluentMap.Analyzers/PublicAPI.Shipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/Dapper.FluentMap.Analyzers/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Dapper.FluentMap.Analyzers/PublicAPI.Unshipped.txt b/src/Dapper.FluentMap.Analyzers/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..3004644 --- /dev/null +++ b/src/Dapper.FluentMap.Analyzers/PublicAPI.Unshipped.txt @@ -0,0 +1,16 @@ +#nullable enable +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicateColumnDiagnosticId = "DFM003" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicateConstructionStrategyDiagnosticId = "DFM016" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicateMemberPathDiagnosticId = "DFM002" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicateProfileRegistrationDiagnosticId = "DFM010" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicatePropertyConverterDiagnosticId = "DFM015" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidGenericMapRegistrationDiagnosticId = "DFM005" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidGenericProfileRegistrationDiagnosticId = "DFM009" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidIncludeBaseDiagnosticId = "DFM004" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidMapExpressionDiagnosticId = "DFM001" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidPersistenceBehaviorDiagnosticId = "DFM013" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidPropertyConverterDiagnosticId = "DFM014" -> string! +Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer +Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.FluentMapConfigurationAnalyzer() -> void +override Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext! context) -> void +override Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.SupportedDiagnostics.get -> System.Collections.Immutable.ImmutableArray diff --git a/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Shipped.txt b/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Shipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Unshipped.txt b/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..283dd36 --- /dev/null +++ b/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +#nullable enable +Microsoft.Extensions.DependencyInjection.FluentMapServiceCollectionExtensions +static Microsoft.Extensions.DependencyInjection.FluentMapServiceCollectionExtensions.AddFluentMap(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Dapper.FluentMap.Dommel/PublicAPI.Shipped.txt b/src/Dapper.FluentMap.Dommel/PublicAPI.Shipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/Dapper.FluentMap.Dommel/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Dapper.FluentMap.Dommel/PublicAPI.Unshipped.txt b/src/Dapper.FluentMap.Dommel/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..d09b0d6 --- /dev/null +++ b/src/Dapper.FluentMap.Dommel/PublicAPI.Unshipped.txt @@ -0,0 +1,39 @@ +#nullable enable +Dapper.FluentMap.Dommel.DommelWriteExtensions +Dapper.FluentMap.Dommel.FluentMapConfigurationExtensions +Dapper.FluentMap.Dommel.Mapping.DommelEntityMap +Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.DommelEntityMap() -> void +Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.TableName.get -> string! +Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.ToTable(string! tableName) -> void +Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.ToTable(string! tableName, string! schemaName) -> void +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.DommelPropertyMap(System.Reflection.PropertyInfo! info) -> void +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.GeneratedOption.get -> System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption? +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.GeneratedOption.set -> void +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.Identity.get -> bool +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.Identity.set -> void +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.IsIdentity() -> Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap! +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.IsKey() -> Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap! +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.Key.get -> bool +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.SetGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption option) -> Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap! +Dapper.FluentMap.Dommel.Mapping.IDommelEntityMap +Dapper.FluentMap.Dommel.Mapping.IDommelEntityMap.TableName.get -> string! +Dapper.FluentMap.Dommel.Resolvers.DommelColumnNameResolver +Dapper.FluentMap.Dommel.Resolvers.DommelColumnNameResolver.DommelColumnNameResolver() -> void +Dapper.FluentMap.Dommel.Resolvers.DommelColumnNameResolver.ResolveColumnName(System.Reflection.PropertyInfo! propertyInfo) -> string! +Dapper.FluentMap.Dommel.Resolvers.DommelKeyPropertyResolver +Dapper.FluentMap.Dommel.Resolvers.DommelKeyPropertyResolver.DommelKeyPropertyResolver() -> void +Dapper.FluentMap.Dommel.Resolvers.DommelKeyPropertyResolver.ResolveKeyProperties(System.Type! type) -> Dommel.ColumnPropertyInfo![]! +Dapper.FluentMap.Dommel.Resolvers.DommelPropertyResolver +Dapper.FluentMap.Dommel.Resolvers.DommelPropertyResolver.DommelPropertyResolver() -> void +Dapper.FluentMap.Dommel.Resolvers.DommelTableNameResolver +Dapper.FluentMap.Dommel.Resolvers.DommelTableNameResolver.DommelTableNameResolver() -> void +Dapper.FluentMap.Dommel.Resolvers.DommelTableNameResolver.ResolveTableName(System.Type! type) -> string! +override Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.GetPropertyMap(System.Reflection.PropertyInfo! info) -> Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap! +override Dapper.FluentMap.Dommel.Resolvers.DommelPropertyResolver.FilterComplexTypes(System.Collections.Generic.IEnumerable! properties) -> System.Collections.Generic.IEnumerable! +override Dapper.FluentMap.Dommel.Resolvers.DommelPropertyResolver.ResolveProperties(System.Type! type) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.Dommel.DommelWriteExtensions.InsertMapped(this System.Data.IDbConnection! connection, TEntity! entity, System.Data.IDbTransaction? transaction = null) -> object? +static Dapper.FluentMap.Dommel.DommelWriteExtensions.InsertMappedAsync(this System.Data.IDbConnection! connection, TEntity! entity, System.Data.IDbTransaction? transaction = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static Dapper.FluentMap.Dommel.DommelWriteExtensions.UpdateMapped(this System.Data.IDbConnection! connection, TEntity! entity, System.Data.IDbTransaction? transaction = null) -> bool +static Dapper.FluentMap.Dommel.DommelWriteExtensions.UpdateMappedAsync(this System.Data.IDbConnection! connection, TEntity! entity, System.Data.IDbTransaction? transaction = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static Dapper.FluentMap.Dommel.FluentMapConfigurationExtensions.ForDommel(this Dapper.FluentMap.Configuration.FluentMapConfiguration! config) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! diff --git a/src/Dapper.FluentMap.Generators/PublicAPI.Shipped.txt b/src/Dapper.FluentMap.Generators/PublicAPI.Shipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/Dapper.FluentMap.Generators/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Dapper.FluentMap.Generators/PublicAPI.Unshipped.txt b/src/Dapper.FluentMap.Generators/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..f6f9b23 --- /dev/null +++ b/src/Dapper.FluentMap.Generators/PublicAPI.Unshipped.txt @@ -0,0 +1,10 @@ +#nullable enable +const Dapper.FluentMap.Generators.MappingRegistrationGenerator.DuplicateGeneratedEntityMapDiagnosticId = "DFM007" -> string! +const Dapper.FluentMap.Generators.MappingRegistrationGenerator.DuplicateGeneratedProfileMapDiagnosticId = "DFM008" -> string! +const Dapper.FluentMap.Generators.MappingRegistrationGenerator.InvalidGeneratedReadConverterDiagnosticId = "DFM012" -> string! +const Dapper.FluentMap.Generators.MappingRegistrationGenerator.InvalidGenericMapRegistrationDiagnosticId = "DFM005" -> string! +const Dapper.FluentMap.Generators.MappingRegistrationGenerator.SkippedGeneratedMapDiagnosticId = "DFM006" -> string! +const Dapper.FluentMap.Generators.MappingRegistrationGenerator.SkippedGeneratedMaterializerDiagnosticId = "DFM011" -> string! +Dapper.FluentMap.Generators.MappingRegistrationGenerator +Dapper.FluentMap.Generators.MappingRegistrationGenerator.Initialize(Microsoft.CodeAnalysis.IncrementalGeneratorInitializationContext context) -> void +Dapper.FluentMap.Generators.MappingRegistrationGenerator.MappingRegistrationGenerator() -> void diff --git a/src/Dapper.FluentMap/Materialization/GeneratedMaterializerColumn.cs b/src/Dapper.FluentMap/Materialization/GeneratedMaterializerColumn.cs index 5610443..088ad18 100644 --- a/src/Dapper.FluentMap/Materialization/GeneratedMaterializerColumn.cs +++ b/src/Dapper.FluentMap/Materialization/GeneratedMaterializerColumn.cs @@ -9,11 +9,11 @@ public sealed class GeneratedMaterializerColumn { private GeneratedMaterializerColumn( string columnName, - string memberPath, + string? memberPath, bool ignored, - Type readConverterType, - Type readConverterDatabaseType, - Type readConverterPropertyType) + Type? readConverterType, + Type? readConverterDatabaseType, + Type? readConverterPropertyType) { if (string.IsNullOrWhiteSpace(columnName)) { diff --git a/src/Dapper.FluentMap/Materialization/GeneratedMaterializerDescriptor.cs b/src/Dapper.FluentMap/Materialization/GeneratedMaterializerDescriptor.cs index 5b9568d..b27c46b 100644 --- a/src/Dapper.FluentMap/Materialization/GeneratedMaterializerDescriptor.cs +++ b/src/Dapper.FluentMap/Materialization/GeneratedMaterializerDescriptor.cs @@ -34,7 +34,7 @@ public GeneratedMaterializerDescriptor( /// The ordered column shape and member bindings expected by the materializer. /// The generated row materializer. public GeneratedMaterializerDescriptor( - Type profileType, + Type? profileType, IEnumerable columns, GeneratedRowMaterializer materializer) { diff --git a/src/Dapper.FluentMap/PublicAPI.Shipped.txt b/src/Dapper.FluentMap/PublicAPI.Shipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/Dapper.FluentMap/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Dapper.FluentMap/PublicAPI.Unshipped.txt b/src/Dapper.FluentMap/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..73112db --- /dev/null +++ b/src/Dapper.FluentMap/PublicAPI.Unshipped.txt @@ -0,0 +1,380 @@ +#nullable enable +abstract Dapper.FluentMap.Mapping.EntityMapBase.GetPropertyMap(System.Reflection.PropertyInfo! info) -> TPropertyMap +Dapper.FluentMap.Configuration.ConventionMappingConfiguration +Dapper.FluentMap.Configuration.ConventionMappingConfiguration.ConventionType.get -> System.Type! +Dapper.FluentMap.Configuration.ConventionMappingConfiguration.EntityType.get -> System.Type! +Dapper.FluentMap.Configuration.ConventionMappingConfiguration.PropertyMaps.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.EntityMappingConfiguration +Dapper.FluentMap.Configuration.EntityMappingConfiguration.EntityType.get -> System.Type! +Dapper.FluentMap.Configuration.EntityMappingConfiguration.IncludedBaseTypes.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.EntityMappingConfiguration.MapType.get -> System.Type! +Dapper.FluentMap.Configuration.EntityMappingConfiguration.PropertyMaps.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.FluentConventionConfiguration +Dapper.FluentMap.Configuration.FluentConventionConfiguration.FluentConventionConfiguration(Dapper.FluentMap.Conventions.Convention! convention) -> void +Dapper.FluentMap.Configuration.FluentConventionConfiguration.ForEntitiesInAssembly(System.Reflection.Assembly! assembly, params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentConventionConfiguration.ForEntitiesInCurrentAssembly(params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentConventionConfiguration.ForEntity() -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentConventionConfiguration.GetType() -> System.Type! +Dapper.FluentMap.Configuration.FluentMapConfiguration +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddConvention() -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddGeneratedMaterializer(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddGeneratedMaterializer(Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor! descriptor) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddGeneratedMaterializer(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddMap(Dapper.FluentMap.Mapping.IEntityMap! mapper) -> void +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddMap() -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddMapsFromAssembly(System.Reflection.Assembly! assembly, params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddMapsFromAssemblyContaining(params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddProfile() -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.FluentMapConfiguration() -> void +Dapper.FluentMap.Configuration.FluentMapConfiguration.GetType() -> System.Type! +Dapper.FluentMap.Configuration.FluentMapConfiguration.UseNamingPolicy(Dapper.FluentMap.Naming.NamingPolicy! namingPolicy, bool caseSensitive = true) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.UseNamingPolicy(System.Func! transformer, bool caseSensitive = true) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddConvention() -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddGeneratedMaterializer(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddGeneratedMaterializer(Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor! descriptor) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddGeneratedMaterializer(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddMap(Dapper.FluentMap.Mapping.IEntityMap! mapper) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddMap() -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddMapsFromAssembly(System.Reflection.Assembly! assembly, params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddMapsFromAssemblyContaining(params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddProfile() -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.Build() -> Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.Configure(System.Action! configure) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.FluentMapConfigurationBuilder() -> void +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.UseNamingPolicy(Dapper.FluentMap.Naming.NamingPolicy! namingPolicy, bool caseSensitive = true) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.UseNamingPolicy(System.Func! transformer, bool caseSensitive = true) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.UseStrictGeneratedMaterialization() -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.Validate() -> void +Dapper.FluentMap.Configuration.GeneratedMaterializerConfiguration +Dapper.FluentMap.Configuration.GeneratedMaterializerConfiguration.Columns.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.GeneratedMaterializerConfiguration.EntityType.get -> System.Type! +Dapper.FluentMap.Configuration.GeneratedMaterializerConfiguration.ProfileType.get -> System.Type! +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.CreateRuntime() -> Dapper.FluentMap.FluentMapRuntime! +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.EntityMaps.get -> System.Collections.Generic.IReadOnlyDictionary! +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.GeneratedMaterializers.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.ProfileMaps.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.StrictGeneratedMaterialization.get -> bool +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.TypeConventions.get -> System.Collections.Generic.IReadOnlyDictionary!>! +Dapper.FluentMap.Configuration.ProfileMappingConfiguration +Dapper.FluentMap.Configuration.ProfileMappingConfiguration.EntityType.get -> System.Type! +Dapper.FluentMap.Configuration.ProfileMappingConfiguration.IncludedBaseTypes.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.ProfileMappingConfiguration.MapType.get -> System.Type! +Dapper.FluentMap.Configuration.ProfileMappingConfiguration.ProfileType.get -> System.Type? +Dapper.FluentMap.Configuration.ProfileMappingConfiguration.PropertyMaps.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.PropertyMappingConfiguration +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.CaseSensitive.get -> bool +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.ColumnName.get -> string! +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.Conversion.get -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.Ignored.get -> bool +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.MemberPath.get -> string! +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.Persistence.get -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.PropertyInfo.get -> System.Reflection.PropertyInfo! +Dapper.FluentMap.Conventions.Convention +Dapper.FluentMap.Conventions.Convention.Convention() -> void +Dapper.FluentMap.Conventions.Convention.ConventionConfigurations.get -> System.Collections.Generic.IList! +Dapper.FluentMap.Conventions.Convention.GetType() -> System.Type! +Dapper.FluentMap.Conventions.Convention.Properties() -> Dapper.FluentMap.Conventions.PropertyConventionConfiguration! +Dapper.FluentMap.Conventions.Convention.Properties() -> Dapper.FluentMap.Conventions.PropertyConventionConfiguration! +Dapper.FluentMap.Conventions.Convention.PropertyMaps.get -> System.Collections.Generic.IList! +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.ConventionPropertyConfiguration() -> void +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.GetType() -> System.Type! +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.HasColumnName(string! columnName) -> Dapper.FluentMap.Conventions.ConventionPropertyConfiguration! +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.HasPrefix(string! prefix) -> Dapper.FluentMap.Conventions.ConventionPropertyConfiguration! +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.IsCaseInsensitive() -> Dapper.FluentMap.Conventions.ConventionPropertyConfiguration! +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.Transform(System.Func! transformer) -> Dapper.FluentMap.Conventions.ConventionPropertyConfiguration! +Dapper.FluentMap.Conventions.PropertyConventionConfiguration +Dapper.FluentMap.Conventions.PropertyConventionConfiguration.Configure(System.Action! configure) -> void +Dapper.FluentMap.Conventions.PropertyConventionConfiguration.GetType() -> System.Type! +Dapper.FluentMap.Conventions.PropertyConventionConfiguration.PropertyConventionConfiguration() -> void +Dapper.FluentMap.Conventions.PropertyConventionConfiguration.Where(System.Func! predicate) -> Dapper.FluentMap.Conventions.PropertyConventionConfiguration! +Dapper.FluentMap.Diagnostics.ConstructorParameterExplanation +Dapper.FluentMap.Diagnostics.ConstructorParameterExplanation.Constructor.get -> System.Reflection.ConstructorInfo! +Dapper.FluentMap.Diagnostics.ConstructorParameterExplanation.Name.get -> string! +Dapper.FluentMap.Diagnostics.ConstructorParameterExplanation.ParameterType.get -> System.Type! +Dapper.FluentMap.Diagnostics.MappingExplanation +Dapper.FluentMap.Diagnostics.MappingExplanation.ConventionTypes.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Diagnostics.MappingExplanation.Diagnostics.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Diagnostics.MappingExplanation.EntityMapType.get -> System.Type? +Dapper.FluentMap.Diagnostics.MappingExplanation.EntityType.get -> System.Type! +Dapper.FluentMap.Diagnostics.MappingExplanation.Members.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Diagnostics.MappingExplanation.ProfileType.get -> System.Type? +Dapper.FluentMap.Diagnostics.MappingMaterialization +Dapper.FluentMap.Diagnostics.MappingMaterialization.Dapper = 0 -> Dapper.FluentMap.Diagnostics.MappingMaterialization +Dapper.FluentMap.Diagnostics.MappingMaterialization.Nested = 1 -> Dapper.FluentMap.Diagnostics.MappingMaterialization +Dapper.FluentMap.Diagnostics.MappingMaterialization.ValueObject = 2 -> Dapper.FluentMap.Diagnostics.MappingMaterialization +Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.Diagnostics.MappingSource.Convention = 2 -> Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.Diagnostics.MappingSource.DapperDefault = 4 -> Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.Diagnostics.MappingSource.Explicit = 0 -> Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.Diagnostics.MappingSource.Inherited = 1 -> Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.Diagnostics.MappingSource.NamingPolicy = 3 -> Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.Diagnostics.MemberMappingExplanation +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.CaseSensitive.get -> bool +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.ColumnName.get -> string! +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.ConstructorParameters.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.ConventionType.get -> System.Type? +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Conversion.get -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Ignored.get -> bool +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.InheritedFrom.get -> System.Type? +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Materialization.get -> Dapper.FluentMap.Diagnostics.MappingMaterialization +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.MemberPath.get -> string! +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Persistence.get -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.PropertyInfo.get -> System.Reflection.PropertyInfo! +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Source.get -> Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.FluentMapConfigurationException +Dapper.FluentMap.FluentMapConfigurationException.FluentMapConfigurationException() -> void +Dapper.FluentMap.FluentMapConfigurationException.FluentMapConfigurationException(string! message) -> void +Dapper.FluentMap.FluentMapConfigurationException.FluentMapConfigurationException(string! message, System.Exception! innerException) -> void +Dapper.FluentMap.FluentMapper +Dapper.FluentMap.FluentMapRuntime +Dapper.FluentMap.FluentMapRuntime.Configuration.get -> Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration! +Dapper.FluentMap.FluentMapRuntime.Explain() -> Dapper.FluentMap.Diagnostics.MappingExplanation! +Dapper.FluentMap.FluentMapRuntime.Explain() -> Dapper.FluentMap.Diagnostics.MappingExplanation! +Dapper.FluentMap.FluentMapRuntime.FluentMapRuntime(Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration! configuration) -> void +Dapper.FluentMap.FluentMapRuntime.QueryGeneratedMapped(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryGeneratedMapped(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryGeneratedMappedSingle(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! +Dapper.FluentMap.FluentMapRuntime.QueryGeneratedMappedSingle(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! +Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMappedAsync(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Dapper.FluentMap.FluentMapRuntime.QueryMappedAsync(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Dapper.FluentMap.FluentMapRuntime.QueryMappedSingle(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! +Dapper.FluentMap.FluentMapRuntime.QueryMappedSingle(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! +Dapper.FluentMap.FluentMapRuntime.QueryMappedUnbuffered(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMappedUnbuffered(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMappedUnbufferedAsync(System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMultipleMapped(System.Data.IDbConnection! connection, string! sql, object? param = null) -> Dapper.FluentMap.MappedGridReader! +Dapper.FluentMap.FluentMapRuntime.QueryMultipleMappedAsync(System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Dapper.FluentMap.FluentMapRuntime.Validate() -> void +Dapper.FluentMap.GeneratedParameters +Dapper.FluentMap.GeneratedParameters.Add(string! name, object? value, System.Data.DbType dbType, int? size = null) -> Dapper.FluentMap.GeneratedParameters! +Dapper.FluentMap.GeneratedParameters.GeneratedParameters() -> void +Dapper.FluentMap.MappedGridReader +Dapper.FluentMap.MappedGridReader.Dispose() -> void +Dapper.FluentMap.MappedGridReader.DisposeAsync() -> System.Threading.Tasks.ValueTask +Dapper.FluentMap.MappedGridReader.IsConsumed.get -> bool +Dapper.FluentMap.MappedGridReader.ReadMapped() -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.MappedGridReader.ReadMapped() -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.MappedGridReader.ReadMappedAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Dapper.FluentMap.MappedGridReader.ReadMappedAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Dapper.FluentMap.MappedGridReader.ReadMappedSingle() -> TEntity! +Dapper.FluentMap.MappedGridReader.ReadMappedSingle() -> TEntity! +Dapper.FluentMap.MappedGridReader.ReadMappedSingleAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Dapper.FluentMap.MappedGridReader.ReadMappedSingleAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Dapper.FluentMap.Mapping.EntityMap +Dapper.FluentMap.Mapping.EntityMap.EntityMap() -> void +Dapper.FluentMap.Mapping.EntityMapBase +Dapper.FluentMap.Mapping.EntityMapBase.ConstructUsing(System.Linq.Expressions.Expression!>! value1, System.Linq.Expressions.Expression!>! value2, System.Linq.Expressions.Expression!>! value3, System.Linq.Expressions.Expression!>! value4, System.Func! factory) -> void +Dapper.FluentMap.Mapping.EntityMapBase.ConstructUsing(System.Linq.Expressions.Expression!>! value1, System.Linq.Expressions.Expression!>! value2, System.Linq.Expressions.Expression!>! value3, System.Func! factory) -> void +Dapper.FluentMap.Mapping.EntityMapBase.ConstructUsing(System.Linq.Expressions.Expression!>! value1, System.Linq.Expressions.Expression!>! value2, System.Func! factory) -> void +Dapper.FluentMap.Mapping.EntityMapBase.ConstructUsing(System.Linq.Expressions.Expression!>! value1, System.Func! factory) -> void +Dapper.FluentMap.Mapping.EntityMapBase.EntityMapBase() -> void +Dapper.FluentMap.Mapping.EntityMapBase.IncludeBase() -> void +Dapper.FluentMap.Mapping.EntityMapBase.Map(System.Linq.Expressions.Expression!>! expression) -> TPropertyMap +Dapper.FluentMap.Mapping.EntityMapBase.PropertyMaps.get -> System.Collections.Generic.IList! +Dapper.FluentMap.Mapping.IEntityMap +Dapper.FluentMap.Mapping.IEntityMap.PropertyMaps.get -> System.Collections.Generic.IList! +Dapper.FluentMap.Mapping.IEntityMap +Dapper.FluentMap.Mapping.IMappingProfile +Dapper.FluentMap.Mapping.IProfileMap +Dapper.FluentMap.Mapping.IPropertyConverter +Dapper.FluentMap.Mapping.IPropertyMap +Dapper.FluentMap.Mapping.IPropertyMap.CaseSensitive.get -> bool +Dapper.FluentMap.Mapping.IPropertyMap.ColumnName.get -> string! +Dapper.FluentMap.Mapping.IPropertyMap.Ignored.get -> bool +Dapper.FluentMap.Mapping.IPropertyMap.PropertyInfo.get -> System.Reflection.PropertyInfo! +Dapper.FluentMap.Mapping.IPropertyMapWithConversionMetadata +Dapper.FluentMap.Mapping.IPropertyMapWithConversionMetadata.Conversion.get -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! +Dapper.FluentMap.Mapping.IPropertyMapWithPersistenceMetadata +Dapper.FluentMap.Mapping.IPropertyMapWithPersistenceMetadata.Persistence.get -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! +Dapper.FluentMap.Mapping.IReadPropertyConverter +Dapper.FluentMap.Mapping.IReadPropertyConverter.ConvertFromDatabase(TDatabase value) -> TProperty +Dapper.FluentMap.Mapping.IWritePropertyConverter +Dapper.FluentMap.Mapping.IWritePropertyConverter.ConvertToDatabase(TProperty value) -> TDatabase +Dapper.FluentMap.Mapping.PropertyConversionDirection +Dapper.FluentMap.Mapping.PropertyConversionDirection.Read = 0 -> Dapper.FluentMap.Mapping.PropertyConversionDirection +Dapper.FluentMap.Mapping.PropertyConversionDirection.Write = 1 -> Dapper.FluentMap.Mapping.PropertyConversionDirection +Dapper.FluentMap.Mapping.PropertyConversionMetadata +Dapper.FluentMap.Mapping.PropertyConversionMetadata.HasReadConverter.get -> bool +Dapper.FluentMap.Mapping.PropertyConversionMetadata.HasWriteConverter.get -> bool +Dapper.FluentMap.Mapping.PropertyConversionMetadata.ReadConverter.get -> Dapper.FluentMap.Mapping.PropertyConverterMetadata? +Dapper.FluentMap.Mapping.PropertyConversionMetadata.WriteConverter.get -> Dapper.FluentMap.Mapping.PropertyConverterMetadata? +Dapper.FluentMap.Mapping.PropertyConverterMetadata +Dapper.FluentMap.Mapping.PropertyConverterMetadata.ConverterType.get -> System.Type! +Dapper.FluentMap.Mapping.PropertyConverterMetadata.DatabaseType.get -> System.Type! +Dapper.FluentMap.Mapping.PropertyConverterMetadata.Direction.get -> Dapper.FluentMap.Mapping.PropertyConversionDirection +Dapper.FluentMap.Mapping.PropertyConverterMetadata.PropertyType.get -> System.Type! +Dapper.FluentMap.Mapping.PropertyMap +Dapper.FluentMap.Mapping.PropertyMap.PropertyMap(System.Reflection.PropertyInfo! info) -> void +Dapper.FluentMap.Mapping.PropertyMap.PropertyMap(System.Reflection.PropertyInfo! info, string! columnName) -> void +Dapper.FluentMap.Mapping.PropertyMap.PropertyMap(System.Reflection.PropertyInfo! info, string! columnName, bool caseSensitive) -> void +Dapper.FluentMap.Mapping.PropertyMapBase +Dapper.FluentMap.Mapping.PropertyMapBase.CaseSensitive.get -> bool +Dapper.FluentMap.Mapping.PropertyMapBase.ColumnName.get -> string! +Dapper.FluentMap.Mapping.PropertyMapBase.Computed() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.Conversion.get -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertFromDatabaseUsing() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertFromDatabaseUsing(Dapper.FluentMap.Mapping.IReadPropertyConverter! converter) -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertFromDatabaseUsing(Dapper.FluentMap.Mapping.ReadPropertyConverter! converter) -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertToDatabaseUsing() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertToDatabaseUsing(Dapper.FluentMap.Mapping.IWritePropertyConverter! converter) -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertToDatabaseUsing(Dapper.FluentMap.Mapping.WritePropertyConverter! converter) -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertUsing() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertUsing(Dapper.FluentMap.Mapping.IPropertyConverter! converter) -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.DatabaseDefaultOnInsert() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ExcludeFromInsert() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ExcludeFromUpdate() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.GetType() -> System.Type! +Dapper.FluentMap.Mapping.PropertyMapBase.Ignore() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.Ignored.get -> bool +Dapper.FluentMap.Mapping.PropertyMapBase.MarkAsComputed() -> void +Dapper.FluentMap.Mapping.PropertyMapBase.MarkAsIdentity() -> void +Dapper.FluentMap.Mapping.PropertyMapBase.MarkAsKey() -> void +Dapper.FluentMap.Mapping.PropertyMapBase.MarkAsNotGenerated() -> void +Dapper.FluentMap.Mapping.PropertyMapBase.Persistence.get -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! +Dapper.FluentMap.Mapping.PropertyMapBase.PropertyInfo.get -> System.Reflection.PropertyInfo! +Dapper.FluentMap.Mapping.PropertyMapBase.PropertyMapBase(System.Reflection.PropertyInfo! info) -> void +Dapper.FluentMap.Mapping.PropertyMapBase.ReadOnly() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ToColumn(string! columnName, bool caseSensitive = true) -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.UsePersistence(Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! persistence) -> void +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.HasDatabaseDefaultOnInsert.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IgnoredByFluentMap.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IsComputed.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IsGenerated.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IsIdentity.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IsKey.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.ParticipatesInInsert.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.ParticipatesInMaterialization.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.ParticipatesInUpdate.get -> bool +Dapper.FluentMap.Mapping.ReadPropertyConverter +Dapper.FluentMap.Mapping.WritePropertyConverter +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.ColumnName.get -> string! +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.Ignored.get -> bool +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.MemberPath.get -> string? +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.ReadConverterDatabaseType.get -> System.Type? +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.ReadConverterPropertyType.get -> System.Type? +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.ReadConverterType.get -> System.Type? +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.Columns.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.EntityType.get -> System.Type! +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.GeneratedMaterializerDescriptor(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> void +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.GeneratedMaterializerDescriptor(System.Type? profileType, System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> void +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.Materializer.get -> Dapper.FluentMap.Materialization.GeneratedRowMaterializer! +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.ProfileType.get -> System.Type? +Dapper.FluentMap.Materialization.GeneratedRowMaterializer +Dapper.FluentMap.Naming.NamingPolicy +Dapper.FluentMap.Naming.NamingPolicy.GetColumnName(string! memberName) -> string! +Dapper.FluentMap.Naming.NamingPolicy.GetType() -> System.Type! +Dapper.FluentMap.Naming.NamingPolicy.Then(Dapper.FluentMap.Naming.NamingPolicy! next) -> Dapper.FluentMap.Naming.NamingPolicy! +Dapper.FluentMap.Naming.NamingPolicy.Then(System.Func! transformer) -> Dapper.FluentMap.Naming.NamingPolicy! +Dapper.FluentMap.Naming.NamingPolicy.WithPrefix(string! prefix) -> Dapper.FluentMap.Naming.NamingPolicy! +Dapper.FluentMap.Naming.NamingPolicy.WithSuffix(string! suffix) -> Dapper.FluentMap.Naming.NamingPolicy! +Dapper.FluentMap.QueryMappedExtensions +Dapper.FluentMap.TypeMaps.FluentConventionTypeMap +Dapper.FluentMap.TypeMaps.FluentConventionTypeMap.FluentConventionTypeMap() -> void +Dapper.FluentMap.TypeMaps.FluentMapTypeMap +Dapper.FluentMap.TypeMaps.FluentMapTypeMap.FluentMapTypeMap() -> void +Dapper.FluentMap.TypeMaps.MultiTypeMap +Dapper.FluentMap.TypeMaps.MultiTypeMap.FindConstructor(string![]! names, System.Type![]! types) -> System.Reflection.ConstructorInfo! +Dapper.FluentMap.TypeMaps.MultiTypeMap.FindExplicitConstructor() -> System.Reflection.ConstructorInfo! +Dapper.FluentMap.TypeMaps.MultiTypeMap.GetConstructorParameter(System.Reflection.ConstructorInfo! constructor, string! columnName) -> Dapper.SqlMapper.IMemberMap! +Dapper.FluentMap.TypeMaps.MultiTypeMap.GetMember(string! columnName) -> Dapper.SqlMapper.IMemberMap! +Dapper.FluentMap.TypeMaps.MultiTypeMap.MultiTypeMap(params Dapper.SqlMapper.ITypeMap![]! mappers) -> void +Dapper.FluentMap.Utils.FluentMapConfigurationExtensions +Dapper.FluentMap.Utils.ReflectionHelper +override Dapper.FluentMap.Configuration.FluentConventionConfiguration.Equals(object! obj) -> bool +override Dapper.FluentMap.Configuration.FluentConventionConfiguration.GetHashCode() -> int +override Dapper.FluentMap.Configuration.FluentConventionConfiguration.ToString() -> string! +override Dapper.FluentMap.Configuration.FluentMapConfiguration.Equals(object! obj) -> bool +override Dapper.FluentMap.Configuration.FluentMapConfiguration.GetHashCode() -> int +override Dapper.FluentMap.Configuration.FluentMapConfiguration.ToString() -> string! +override Dapper.FluentMap.Conventions.Convention.Equals(object! obj) -> bool +override Dapper.FluentMap.Conventions.Convention.GetHashCode() -> int +override Dapper.FluentMap.Conventions.Convention.ToString() -> string! +override Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.Equals(object! obj) -> bool +override Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.GetHashCode() -> int +override Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.ToString() -> string! +override Dapper.FluentMap.Conventions.PropertyConventionConfiguration.Equals(object! obj) -> bool +override Dapper.FluentMap.Conventions.PropertyConventionConfiguration.GetHashCode() -> int +override Dapper.FluentMap.Conventions.PropertyConventionConfiguration.ToString() -> string! +override Dapper.FluentMap.Diagnostics.MappingExplanation.ToString() -> string! +override Dapper.FluentMap.Mapping.EntityMap.GetPropertyMap(System.Reflection.PropertyInfo! info) -> Dapper.FluentMap.Mapping.PropertyMap! +override Dapper.FluentMap.Mapping.PropertyMapBase.Equals(object! obj) -> bool +override Dapper.FluentMap.Mapping.PropertyMapBase.GetHashCode() -> int +override Dapper.FluentMap.Mapping.PropertyMapBase.ToString() -> string! +override Dapper.FluentMap.Naming.NamingPolicy.Equals(object! obj) -> bool +override Dapper.FluentMap.Naming.NamingPolicy.GetHashCode() -> int +override Dapper.FluentMap.Naming.NamingPolicy.ToString() -> string! +static Dapper.FluentMap.FluentMapper.Configuration.get -> Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration! +static Dapper.FluentMap.FluentMapper.Explain() -> Dapper.FluentMap.Diagnostics.MappingExplanation! +static Dapper.FluentMap.FluentMapper.Explain() -> Dapper.FluentMap.Diagnostics.MappingExplanation! +static Dapper.FluentMap.FluentMapper.GetEntityMaps() -> System.Collections.Generic.IReadOnlyDictionary! +static Dapper.FluentMap.FluentMapper.GetTypeConventions() -> System.Collections.Generic.IReadOnlyDictionary!>! +static Dapper.FluentMap.FluentMapper.Initialize(System.Action! configure) -> void +static Dapper.FluentMap.FluentMapper.Runtime.get -> Dapper.FluentMap.FluentMapRuntime! +static Dapper.FluentMap.FluentMapper.Validate() -> void +static Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.Ignore(string! columnName) -> Dapper.FluentMap.Materialization.GeneratedMaterializerColumn! +static Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.Map(string! columnName, string! memberPath) -> Dapper.FluentMap.Materialization.GeneratedMaterializerColumn! +static Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.Map(string! columnName, string! memberPath, System.Type! readConverterType, System.Type! readConverterDatabaseType, System.Type! readConverterPropertyType) -> Dapper.FluentMap.Materialization.GeneratedMaterializerColumn! +static Dapper.FluentMap.Naming.NamingPolicy.Custom(System.Func! transformer) -> Dapper.FluentMap.Naming.NamingPolicy! +static Dapper.FluentMap.Naming.NamingPolicy.Identity.get -> Dapper.FluentMap.Naming.NamingPolicy! +static Dapper.FluentMap.Naming.NamingPolicy.Prefix(string! prefix) -> Dapper.FluentMap.Naming.NamingPolicy! +static Dapper.FluentMap.Naming.NamingPolicy.SnakeCase.get -> Dapper.FluentMap.Naming.NamingPolicy! +static Dapper.FluentMap.Naming.NamingPolicy.Suffix(string! suffix) -> Dapper.FluentMap.Naming.NamingPolicy! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string? splitOn = null) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task!>! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn) -> System.Threading.Tasks.Task!>! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn) -> System.Threading.Tasks.Task!>! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedSingle(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedSingle(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedSingleAsync(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbuffered(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbuffered(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbuffered(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbuffered(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, string! sql, System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, string! sql, System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMultipleMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> Dapper.FluentMap.MappedGridReader! +static Dapper.FluentMap.QueryMappedExtensions.QueryMultipleMapped(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> Dapper.FluentMap.MappedGridReader! +static Dapper.FluentMap.QueryMappedExtensions.QueryMultipleMappedAsync(this System.Data.Common.DbConnection! connection, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.FluentMap.QueryMappedExtensions.QueryMultipleMappedAsync(this System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static Dapper.FluentMap.TypeMaps.MultiTypeMap.MatchColumnNames(Dapper.FluentMap.Mapping.IPropertyMap! map, string! columnName) -> bool +static Dapper.FluentMap.TypeMaps.MultiTypeMap.TypePropertyMapCache.get -> System.Collections.Concurrent.ConcurrentDictionary! +static Dapper.FluentMap.Utils.FluentMapConfigurationExtensions.ApplyMapsFromAssemblies(this Dapper.FluentMap.Configuration.FluentMapConfiguration! configuration, params System.Reflection.Assembly![]! assemblies) -> void +static Dapper.FluentMap.Utils.ReflectionHelper.GetMemberInfo(System.Linq.Expressions.LambdaExpression! lambda) -> System.Reflection.MemberInfo! +static readonly Dapper.FluentMap.FluentMapper.EntityMaps -> System.Collections.Concurrent.ConcurrentDictionary! +static readonly Dapper.FluentMap.FluentMapper.TypeConventions -> System.Collections.Concurrent.ConcurrentDictionary!>! +static readonly Dapper.FluentMap.Mapping.PropertyConversionMetadata.Default -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! +static readonly Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.Default -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! +static readonly Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.Ignored -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! +virtual Dapper.FluentMap.Mapping.ReadPropertyConverter.Invoke(TDatabase value) -> TProperty +virtual Dapper.FluentMap.Mapping.WritePropertyConverter.Invoke(TProperty value) -> TDatabase +virtual Dapper.FluentMap.Materialization.GeneratedRowMaterializer.Invoke(System.Data.IDataRecord! record) -> TEntity! From 30839b1e4f25ec90f2b5e80a497f8a1d42b6f1d4 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Wed, 30 Sep 2026 14:05:06 -0300 Subject: [PATCH 6/9] docs: reconcile roadmap governance evidence --- MAINTAINING.md | 2 +- MIGRATION.pt-BR.md | 4 +++- USAGE.pt-BR.md | 9 +++++++-- benchmarks/baselines/materialization-windows-net10.json | 2 ++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/MAINTAINING.md b/MAINTAINING.md index 4b9c665..953b785 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -36,4 +36,4 @@ The canary is early-warning evidence. A green canary does not add a supported ve `Performance Regression Report` runs the materialization hot paths on a scheduled/manual Windows hosted runner, exports BenchmarkDotNet CSV/JSON/Markdown/HTML, compares ShortRun results with `benchmarks/baselines/materialization-windows-net10.json`, and uploads both raw and comparison artifacts for 90 days. -The initial signals are report-only: +35% mean time or +15% allocated bytes is highlighted for review. The time threshold reflects observed hosted-runner variance; allocation is more stable but still not a required PR gate. Reproduce a signal on controlled hardware before treating it as a regression. Update the checked-in baseline only after reviewing the code change, environment/runtime change, repeated measurements and generated comparison artifact. +The initial signals are report-only: +35% mean time or +15% allocated bytes is highlighted for review. Two initial Windows ShortRun measurements showed substantial timing variance while allocation counts remained stable; scheduled hosted-runner history is not yet sufficient for a hard gate. Reproduce a signal on controlled hardware before treating it as a regression. Update the checked-in baseline only after reviewing the code change, environment/runtime change, repeated measurements and generated comparison artifact. diff --git a/MIGRATION.pt-BR.md b/MIGRATION.pt-BR.md index 9e2ac33..b79fe5b 100644 --- a/MIGRATION.pt-BR.md +++ b/MIGRATION.pt-BR.md @@ -53,7 +53,7 @@ Antes de alterar as versões dos pacotes FluentMap: - 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: +As faixas suportadas atualmente ficam documentadas em [COMPATIBILITY.md](COMPATIBILITY.md). Os valores do Dapper são governados por `eng/compatibility-contract.json` e validados contra metadata de pacote e CI. No momento deste guia: ```text Dapper [2.1.79,3.0.0) @@ -357,6 +357,8 @@ Isso pode substituir registro manual para maps elegíveis da compilação atual. 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. +Queries strict generated podem usar `GeneratedParameters` para comandos parametrizados. Cada valor possui um `DbType` explícito; objetos anônimos e outros bags de parâmetros arbitrários continuam não suportados, para que o modo estrito não introduza descoberta de parâmetros por reflection. Colunas reordenadas e colunas adicionais não mapeadas seguras podem permanecer no caminho gerado; colunas obrigatórias ausentes, nomes duplicados/ambíguos ou colunas adicionais mapeadas falham deterministicamente. + ## 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: diff --git a/USAGE.pt-BR.md b/USAGE.pt-BR.md index 1485f83..4498211 100644 --- a/USAGE.pt-BR.md +++ b/USAGE.pt-BR.md @@ -404,10 +404,15 @@ var runtime = new FluentMapConfigurationBuilder() var customer = runtime.QueryGeneratedMappedSingle( connection, - "SELECT 7 AS customer_id, 'Ada' AS customer_name;"); + "SELECT @Id AS customer_id, @Name AS customer_name, 'trace' AS trace_id;", + new GeneratedParameters() + .Add("Id", 7, System.Data.DbType.Int32) + .Add("Name", "Ada", System.Data.DbType.String, size: 100)); ``` -Shapes não suportados falham deterministicamente em vez de usar silenciosamente materialização runtime. +`GeneratedParameters` vincula somente valores de entrada nomeados explicitamente e com `DbType` explícito; ele não inspeciona objetos anônimos. Comandos sem parâmetros continuam suportados. Objetos de parâmetros arbitrários falham antes de abrir a conexão, e o diagnóstico identifica `QueryMapped*` como alternativa não estrita. + +Materializadores gerados aceitam colunas obrigatórias reordenadas e colunas adicionais somente quando os nomes adicionais não resolvem membros mapeados. Colunas obrigatórias ausentes, nomes duplicados, colunas adicionais mapeadas, valores CLR incompatíveis do provider e shapes aninhados não suportados falham deterministicamente, sem fallback silencioso para 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. diff --git a/benchmarks/baselines/materialization-windows-net10.json b/benchmarks/baselines/materialization-windows-net10.json index 7da9e71..e0cabc5 100644 --- a/benchmarks/baselines/materialization-windows-net10.json +++ b/benchmarks/baselines/materialization-windows-net10.json @@ -3,6 +3,8 @@ "runtime": ".NET 10", "operatingSystem": "Windows", "job": "ShortRun", + "calibrationRuns": 2, + "calibrationNote": "Initial Windows ShortRun measurements showed material timing variance while allocations remained stable; thresholds are report-only pending scheduled hosted-runner history.", "timeRegressionThresholdPercent": 35, "allocationRegressionThresholdPercent": 15, "reportOnly": true, From af515c7e3616f1b201bcb552beb1f7d56855e160 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Wed, 30 Sep 2026 14:34:31 -0300 Subject: [PATCH 7/9] fix: harden roadmap CI and strict AOT paths --- .../DommelWriteExtensions.cs | 8 +- .../Resolvers/DommelPersistenceMetadata.cs | 26 +++- src/Dapper.FluentMap/FluentMapRuntime.cs | 28 ++-- src/Dapper.FluentMap/MappingRegistry.cs | 57 +++++-- src/Dapper.FluentMap/QueryMappedExtensions.cs | 88 ++++++----- .../ExplicitConstructionStrategyTests.cs | 145 ++++++++++++++++++ 6 files changed, 276 insertions(+), 76 deletions(-) diff --git a/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs b/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs index 8863464..d149a82 100644 --- a/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs +++ b/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -110,6 +111,10 @@ private static CommandDefinition CreateInsertCommand( return new CommandDefinition(sql, parameters, transaction, cancellationToken: cancellationToken); } + [SuppressMessage( + "Security", + "S2077:Formatting SQL queries is security-sensitive", + Justification = "Only provider-quoted table and column identifiers are composed here; every entity value remains a Dapper parameter.")] private static CommandDefinition CreateUpdateCommand( IDbConnection connection, TEntity entity, @@ -161,6 +166,7 @@ private static DynamicParameters CreateParameters( var conversion = propertyMap == null ? PropertyConversionMetadata.Default : PropertyMapConversion.GetConversion(propertyMap); + var columnName = propertyMap == null ? property.Name : propertyMap.ColumnName; if (conversion.HasWriteConverter && value != null) { @@ -171,7 +177,7 @@ private static DynamicParameters CreateParameters( catch (Exception exception) { throw new FluentMapConfigurationException( - $"Write converter failed during Dommel {operation} for entity '{entityType.FullName}', property '{property.Name}', column '{propertyMap.ColumnName}', converter '{conversion.WriteConverter.ConverterType.FullName}'. The original property value was not written.", + $"Write converter failed during Dommel {operation} for entity '{entityType.FullName}', property '{property.Name}', column '{columnName}', converter '{conversion.WriteConverter.ConverterType.FullName}'. The original property value was not written.", exception); } } diff --git a/src/Dapper.FluentMap.Dommel/Resolvers/DommelPersistenceMetadata.cs b/src/Dapper.FluentMap.Dommel/Resolvers/DommelPersistenceMetadata.cs index 52b829b..c64bbc2 100644 --- a/src/Dapper.FluentMap.Dommel/Resolvers/DommelPersistenceMetadata.cs +++ b/src/Dapper.FluentMap.Dommel/Resolvers/DommelPersistenceMetadata.cs @@ -26,9 +26,16 @@ internal static bool TryGetPersistence(PropertyInfo property, out PropertyPersis } var mapWithPersistence = propertyMap as IPropertyMapWithPersistenceMetadata; - persistence = mapWithPersistence == null - ? (propertyMap.Ignored ? PropertyPersistenceMetadata.Ignored : PropertyPersistenceMetadata.Default) - : mapWithPersistence.Persistence; + if (mapWithPersistence != null) + { + persistence = mapWithPersistence.Persistence; + } + else + { + persistence = propertyMap.Ignored + ? PropertyPersistenceMetadata.Ignored + : PropertyPersistenceMetadata.Default; + } return true; } @@ -52,14 +59,21 @@ internal static IEnumerable ResolveInsertProperties(Type type) } var mapWithPersistence = propertyMap as IPropertyMapWithPersistenceMetadata; - var persistence = mapWithPersistence == null - ? (propertyMap.Ignored ? PropertyPersistenceMetadata.Ignored : PropertyPersistenceMetadata.Default) - : mapWithPersistence.Persistence; + var persistence = mapWithPersistence != null + ? mapWithPersistence.Persistence + : ResolveDefaultPersistence(propertyMap); return persistence.ParticipatesInInsert; }) .Select(property => property.Property); } + private static PropertyPersistenceMetadata ResolveDefaultPersistence(IPropertyMap propertyMap) + { + return propertyMap.Ignored + ? PropertyPersistenceMetadata.Ignored + : PropertyPersistenceMetadata.Default; + } + internal static IPropertyMap ResolvePropertyMap(Type type, IEntityMap entityMap, string propertyName) { return ResolvePropertyMaps(type, entityMap).FirstOrDefault(map => map.PropertyInfo.Name == propertyName); diff --git a/src/Dapper.FluentMap/FluentMapRuntime.cs b/src/Dapper.FluentMap/FluentMapRuntime.cs index ae759ab..ad1ab64 100644 --- a/src/Dapper.FluentMap/FluentMapRuntime.cs +++ b/src/Dapper.FluentMap/FluentMapRuntime.cs @@ -256,18 +256,14 @@ public Task> QueryMappedAsync< throw new ArgumentNullException(nameof(sql)); } - if (map == null) - { - throw new ArgumentNullException(nameof(map)); - } - - return QueryMappedExtensions.ExecuteMappedSegmentsAsync( + return QueryMappedExtensions.ExecuteMappedAsync( connection, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, cancellationToken), - new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, - new Type[] { null, null, null }, - values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + map, splitOn, + null, + null, + null, this); } @@ -305,18 +301,14 @@ public Task> QueryMappedAsync< throw new ArgumentNullException(nameof(sql)); } - if (map == null) - { - throw new ArgumentNullException(nameof(map)); - } - - return QueryMappedExtensions.ExecuteMappedSegmentsAsync( + return QueryMappedExtensions.ExecuteMappedAsync( connection, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, cancellationToken), - new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, - new[] { typeof(TFirstProfile), typeof(TSecondProfile), typeof(TThirdProfile) }, - values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + map, splitOn, + typeof(TFirstProfile), + typeof(TSecondProfile), + typeof(TThirdProfile), this); } diff --git a/src/Dapper.FluentMap/MappingRegistry.cs b/src/Dapper.FluentMap/MappingRegistry.cs index 3593e1a..166f542 100644 --- a/src/Dapper.FluentMap/MappingRegistry.cs +++ b/src/Dapper.FluentMap/MappingRegistry.cs @@ -381,11 +381,10 @@ internal bool TryGetGeneratedMaterializer( return true; } - if (TryCreateCompatibleGeneratedMaterializer( + if (TryCreateRuntimeCompatibleGeneratedMaterializer( type, profileType, columnNames, - requireExplicitMapping: false, out entry)) { _generatedMaterializerResolutionCache.TryAdd(cacheKey, entry); @@ -419,11 +418,10 @@ internal bool TryGetStrictGeneratedMaterializer( return true; } - if (TryCreateCompatibleGeneratedMaterializer( + if (TryCreateStrictCompatibleGeneratedMaterializer( type, profileType, columnNames, - requireExplicitMapping: true, out entry)) { materializer = entry.Materialize; @@ -451,11 +449,43 @@ private bool GeneratedMaterializerMatchesExplicitMapping( return true; } + private bool TryCreateRuntimeCompatibleGeneratedMaterializer( + Type type, + Type profileType, + string[] columnNames, + out GeneratedMaterializerEntry reorderedEntry) + { + var defaultTypeMap = new DefaultTypeMap(type); + return TryCreateCompatibleGeneratedMaterializer( + type, + profileType, + columnNames, + columns => GeneratedMaterializerMatchesEffectiveMapping(type, profileType, columns), + columnName => defaultTypeMap.GetMember(columnName) != null, + out reorderedEntry); + } + + private bool TryCreateStrictCompatibleGeneratedMaterializer( + Type type, + Type profileType, + string[] columnNames, + out GeneratedMaterializerEntry reorderedEntry) + { + return TryCreateCompatibleGeneratedMaterializer( + type, + profileType, + columnNames, + columns => GeneratedMaterializerMatchesExplicitMapping(type, profileType, columns), + defaultMemberResolver: null, + out reorderedEntry); + } + private bool TryCreateCompatibleGeneratedMaterializer( Type type, Type profileType, string[] columnNames, - bool requireExplicitMapping, + Func, bool> mappingMatches, + Func defaultMemberResolver, out GeneratedMaterializerEntry reorderedEntry) { reorderedEntry = null; @@ -479,10 +509,8 @@ private bool TryCreateCompatibleGeneratedMaterializer( profileType, generatedColumnNames, columnNames, - includeDapperDefaultMappings: !requireExplicitMapping) || - !(requireExplicitMapping - ? GeneratedMaterializerMatchesExplicitMapping(type, profileType, candidate.Value.Columns) - : GeneratedMaterializerMatchesEffectiveMapping(type, profileType, candidate.Value.Columns))) + defaultMemberResolver) || + !mappingMatches(candidate.Value.Columns)) { continue; } @@ -560,7 +588,7 @@ internal string DescribeMissingGeneratedMaterializer(Type type, Type profileType profileType, candidate.Key.ColumnNames, columnNames, - includeDapperDefaultMappings: false); + defaultMemberResolver: null); if (unsafeAdditionalColumns.Count > 0) { return $"Entity '{type.FullName}' ({profileContext}) requested result columns {requestedShape} contain additional columns that resolve to mapped members and cannot be ignored safely: {FormatColumnShape(unsafeAdditionalColumns)}."; @@ -894,14 +922,14 @@ private bool AdditionalColumnsAreUnmapped( Type profileType, IReadOnlyList generatedColumnNames, IEnumerable requestedColumnNames, - bool includeDapperDefaultMappings) + Func defaultMemberResolver) { return GetUnsafeAdditionalColumns( type, profileType, generatedColumnNames, requestedColumnNames, - includeDapperDefaultMappings).Count == 0; + defaultMemberResolver).Count == 0; } private List GetUnsafeAdditionalColumns( @@ -909,11 +937,10 @@ private List GetUnsafeAdditionalColumns( Type profileType, IReadOnlyList generatedColumnNames, IEnumerable requestedColumnNames, - bool includeDapperDefaultMappings) + Func defaultMemberResolver) { var generated = new HashSet(generatedColumnNames, StringComparer.Ordinal); var explicitPropertyMaps = GetExplicitPropertyMaps(type, profileType); - var defaultTypeMap = includeDapperDefaultMappings ? new DefaultTypeMap(type) : null; var unsafeColumns = new List(); foreach (var columnName in requestedColumnNames.Where(columnName => !generated.Contains(columnName))) @@ -924,7 +951,7 @@ private List GetUnsafeAdditionalColumns( string.Equals(map.PropertyInfo.Name, columnName, StringComparison.OrdinalIgnoreCase)); if ((fluentMap != null && !fluentMap.Ignored) || matchesMappedMemberName || - (fluentMap == null && defaultTypeMap != null && defaultTypeMap.GetMember(columnName) != null)) + (fluentMap == null && defaultMemberResolver != null && defaultMemberResolver(columnName))) { unsafeColumns.Add(columnName); } diff --git a/src/Dapper.FluentMap/QueryMappedExtensions.cs b/src/Dapper.FluentMap/QueryMappedExtensions.cs index 855f7ab..77f6836 100644 --- a/src/Dapper.FluentMap/QueryMappedExtensions.cs +++ b/src/Dapper.FluentMap/QueryMappedExtensions.cs @@ -847,18 +847,14 @@ public static Task> QueryMappedAsync< throw new ArgumentNullException(nameof(sql)); } - if (map == null) - { - throw new ArgumentNullException(nameof(map)); - } - - return ExecuteMappedSegmentsAsync( + return ExecuteMappedAsync( connection, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, cancellationToken), - new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, - new Type[] { null, null, null }, - values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + map, splitOn, + null, + null, + null, FluentMapper.Runtime); } @@ -880,18 +876,14 @@ public static Task> QueryMappedAsync< where TSecond : class where TThird : class { - if (map == null) - { - throw new ArgumentNullException(nameof(map)); - } - - return ExecuteMappedSegmentsAsync( + return ExecuteMappedAsync( connection, command, - new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, - new Type[] { null, null, null }, - values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + map, splitOn, + null, + null, + null, FluentMapper.Runtime); } @@ -929,18 +921,14 @@ public static Task> QueryMappedAsync< throw new ArgumentNullException(nameof(sql)); } - if (map == null) - { - throw new ArgumentNullException(nameof(map)); - } - - return ExecuteMappedSegmentsAsync( + return ExecuteMappedAsync( connection, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, cancellationToken), - new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, - new[] { typeof(TFirstProfile), typeof(TSecondProfile), typeof(TThirdProfile) }, - values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + map, splitOn, + typeof(TFirstProfile), + typeof(TSecondProfile), + typeof(TThirdProfile), FluentMapper.Runtime); } @@ -968,18 +956,14 @@ public static Task> QueryMappedAsync< where TSecondProfile : IMappingProfile where TThirdProfile : IMappingProfile { - if (map == null) - { - throw new ArgumentNullException(nameof(map)); - } - - return ExecuteMappedSegmentsAsync( + return ExecuteMappedAsync( connection, command, - new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, - new[] { typeof(TFirstProfile), typeof(TSecondProfile), typeof(TThirdProfile) }, - values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + map, splitOn, + typeof(TFirstProfile), + typeof(TSecondProfile), + typeof(TThirdProfile), FluentMapper.Runtime); } @@ -1309,6 +1293,38 @@ internal static IEnumerable ExecuteMapped< runtime); } + internal static Task> ExecuteMappedAsync< + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TSecond, + [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TThird, + TReturn>( + IDbConnection connection, + CommandDefinition command, + Func map, + string splitOn, + Type firstProfileType, + Type secondProfileType, + Type thirdProfileType, + FluentMapRuntime runtime) + where TFirst : class + where TSecond : class + where TThird : class + { + if (map == null) + { + throw new ArgumentNullException(nameof(map)); + } + + return ExecuteMappedSegmentsAsync( + connection, + command, + new[] { typeof(TFirst), typeof(TSecond), typeof(TThird) }, + new[] { firstProfileType, secondProfileType, thirdProfileType }, + values => map((TFirst)values[0], (TSecond)values[1], (TThird)values[2]), + splitOn, + runtime); + } + private static IEnumerable ExecuteMappedSegments( IDbConnection connection, CommandDefinition command, diff --git a/test/Dapper.FluentMap.Tests/ExplicitConstructionStrategyTests.cs b/test/Dapper.FluentMap.Tests/ExplicitConstructionStrategyTests.cs index 60773f0..d4be789 100644 --- a/test/Dapper.FluentMap.Tests/ExplicitConstructionStrategyTests.cs +++ b/test/Dapper.FluentMap.Tests/ExplicitConstructionStrategyTests.cs @@ -83,6 +83,36 @@ public void DuplicateConstructionStrategyShouldFailDuringMapConfiguration() Assert.Contains("already has", exception.Message); } + [Fact] + public void ConstructionFactoriesShouldPreserveBindingOrderAcrossSupportedArities() + { + var oneValueStrategy = GetConstructionStrategy(new OneValueFactoryCustomerMap()); + var threeValueStrategy = GetConstructionStrategy(new ThreeValueFactoryAggregateMap()); + var fourValueStrategy = GetConstructionStrategy(new FourValueFactoryAggregateMap()); + + var customer = (FactoryCustomer)oneValueStrategy.Factory(new object[] { 21 }); + var threeValue = (FactoryAggregate)threeValueStrategy.Factory(new object[] { 1, "two", 3L }); + var fourValue = (FactoryAggregate)fourValueStrategy.Factory(new object[] { 4, "five", 6L, true }); + + Assert.Equal(21, customer.Id); + Assert.Equal(new[] { "Id" }, GetBindingPaths(oneValueStrategy)); + Assert.Equal((1, "two", 3L, false), (threeValue.First, threeValue.Second, threeValue.Third, threeValue.Fourth)); + Assert.Equal(new[] { "First", "Second", "Third" }, GetBindingPaths(threeValueStrategy)); + Assert.Equal((4, "five", 6L, true), (fourValue.First, fourValue.Second, fourValue.Third, fourValue.Fourth)); + Assert.Equal(new[] { "First", "Second", "Third", "Fourth" }, GetBindingPaths(fourValueStrategy)); + } + + [Fact] + public void ConstructUsingShouldRejectNullFactoriesAcrossSupportedArities() + { + var map = new ConstructionFactoryProbeMap(); + + Assert.Equal("factory", Assert.Throws(() => map.ConfigureOne(null)).ParamName); + Assert.Equal("factory", Assert.Throws(() => map.ConfigureTwo(null)).ParamName); + Assert.Equal("factory", Assert.Throws(() => map.ConfigureThree(null)).ParamName); + Assert.Equal("factory", Assert.Throws(() => map.ConfigureFour(null)).ParamName); + } + [Fact] [Trait("Category", "Integration")] public void FactoryFailureShouldIncludeEntityAndBoundPropertyContext() @@ -135,6 +165,23 @@ private static SqliteConnection OpenConnection() return connection; } + private static EntityConstructionStrategy GetConstructionStrategy(EntityMap map) + where TEntity : class + { + return ((IEntityMapWithConstructionStrategy)map).ConstructionStrategy; + } + + private static string[] GetBindingPaths(EntityConstructionStrategy strategy) + { + var paths = new string[strategy.Bindings.Count]; + for (var index = 0; index < strategy.Bindings.Count; index++) + { + paths[index] = strategy.Bindings[index].MemberPath.ToString(); + } + + return paths; + } + private sealed class FactoryCustomer { private FactoryCustomer(int id, string name) @@ -180,6 +227,104 @@ public IncompleteFactoryCustomerMap() } } + private sealed class OneValueFactoryCustomerMap : EntityMap + { + public OneValueFactoryCustomerMap() + { + Map(customer => customer.Id).ToColumn("customer_id"); + ConstructUsing(customer => customer.Id, FactoryCustomer.RestoreIdOnly); + } + } + + private sealed class FactoryAggregate + { + private FactoryAggregate(int first, string second, long third, bool fourth) + { + First = first; + Second = second; + Third = third; + Fourth = fourth; + } + + public int First { get; } + + public string Second { get; } + + public long Third { get; } + + public bool Fourth { get; } + + internal static FactoryAggregate RestoreThree(int first, string second, long third) + { + return new FactoryAggregate(first, second, third, false); + } + + internal static FactoryAggregate RestoreFour(int first, string second, long third, bool fourth) + { + return new FactoryAggregate(first, second, third, fourth); + } + } + + private sealed class ThreeValueFactoryAggregateMap : EntityMap + { + public ThreeValueFactoryAggregateMap() + { + Map(entity => entity.First); + Map(entity => entity.Second); + Map(entity => entity.Third); + ConstructUsing( + entity => entity.First, + entity => entity.Second, + entity => entity.Third, + FactoryAggregate.RestoreThree); + } + } + + private sealed class FourValueFactoryAggregateMap : EntityMap + { + public FourValueFactoryAggregateMap() + { + Map(entity => entity.First); + Map(entity => entity.Second); + Map(entity => entity.Third); + Map(entity => entity.Fourth); + ConstructUsing( + entity => entity.First, + entity => entity.Second, + entity => entity.Third, + entity => entity.Fourth, + FactoryAggregate.RestoreFour); + } + } + + private sealed class ConstructionFactoryProbeMap : EntityMap + { + internal void ConfigureOne(Func factory) + { + ConstructUsing(entity => entity.First, factory); + } + + internal void ConfigureTwo(Func factory) + { + ConstructUsing(entity => entity.First, entity => entity.Second, factory); + } + + internal void ConfigureThree(Func factory) + { + ConstructUsing(entity => entity.First, entity => entity.Second, entity => entity.Third, factory); + } + + internal void ConfigureFour(Func factory) + { + ConstructUsing( + entity => entity.First, + entity => entity.Second, + entity => entity.Third, + entity => entity.Fourth, + factory); + } + } + private sealed class DuplicateFactoryCustomerMap : FactoryCustomerMap { public DuplicateFactoryCustomerMap() From e9930efb9d029aefb32d3f92571bb103885df9f8 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Wed, 30 Sep 2026 15:15:26 -0300 Subject: [PATCH 8/9] fix: resolve automated roadmap review findings --- .github/workflows/performance-report.yml | 33 +- CHANGELOG.md | 3 +- COMPATIBILITY.md | 2 +- MAINTAINING.md | 2 +- MIGRATION.md | 9 +- MIGRATION.pt-BR.md | 9 +- USAGE.md | 4 +- USAGE.pt-BR.md | 4 +- eng/compare-benchmark-results.ps1 | 18 +- .../FluentMapConfigurationAnalyzer.cs | 41 +- .../PublicAPI.Shipped.txt | 15 + .../PublicAPI.Unshipped.txt | 14 - .../PublicAPI.Shipped.txt | 3 + .../PublicAPI.Unshipped.txt | 2 - .../DommelWriteExtensions.cs | 22 +- .../PublicAPI.Shipped.txt | 34 ++ .../PublicAPI.Unshipped.txt | 33 -- .../MappingRegistrationGenerator.cs | 2 +- .../PublicAPI.Shipped.txt | 10 + .../PublicAPI.Unshipped.txt | 9 - .../ImmutableFluentMapConfiguration.cs | 10 +- src/Dapper.FluentMap/FluentMapRuntime.cs | 12 +- .../MappingConfigurationValidator.cs | 6 + src/Dapper.FluentMap/MappingRegistry.cs | 16 + src/Dapper.FluentMap/PublicAPI.Shipped.txt | 361 +++++++++++++++++ src/Dapper.FluentMap/PublicAPI.Unshipped.txt | 378 +----------------- src/Dapper.FluentMap/QueryMappedExtensions.cs | 60 ++- .../FluentMapConfigurationAnalyzerTests.cs | 38 ++ .../DommelPersistenceIntegrationTests.cs | 73 +++- .../ExplicitConstructionStrategyTests.cs | 48 +++ .../QueryMappedMultiMappingTests.cs | 47 ++- 31 files changed, 815 insertions(+), 503 deletions(-) diff --git a/.github/workflows/performance-report.yml b/.github/workflows/performance-report.yml index 3df8e8d..0efec72 100644 --- a/.github/workflows/performance-report.yml +++ b/.github/workflows/performance-report.yml @@ -45,12 +45,41 @@ jobs: --filter "*DapperWithFluentMapRootMapping*" "*QueryMappedSimple" "*RuntimeQueryMappedSimple" "*RuntimeQueryGeneratedSimpleExtraColumn*" "*QueryMappedGeneratedPropertyConverter*" "*QueryMappedTwoSegments*" "*QueryMappedThreeSegments*" - --exporters json + --exporters fulljson + + - name: Capture benchmark run metadata + shell: pwsh + env: + BENCHMARK_COMMAND: >- + dotnet run --project ./benchmarks/Dapper.FluentMap.Benchmarks/Dapper.FluentMap.Benchmarks.csproj + --configuration Release --no-build -- + --filter "*DapperWithFluentMapRootMapping*" "*QueryMappedSimple" "*RuntimeQueryMappedSimple" + "*RuntimeQueryGeneratedSimpleExtraColumn*" "*QueryMappedGeneratedPropertyConverter*" + "*QueryMappedTwoSegments*" "*QueryMappedThreeSegments*" + --exporters fulljson + COMMIT_SHA: "${{ github.sha }}" + RUNNER_NAME: "${{ runner.name }}" + RUNNER_OS: "${{ runner.os }}" + RUNNER_ARCH: "${{ runner.arch }}" + run: | + New-Item -ItemType Directory -Force -Path './artifacts/benchmark-report' | Out-Null + $metadata = [ordered]@{ + command = $env:BENCHMARK_COMMAND + commit = $env:COMMIT_SHA + runner = [ordered]@{ + name = $env:RUNNER_NAME + os = $env:RUNNER_OS + architecture = $env:RUNNER_ARCH + } + dotnetInfo = ((dotnet --info) -join [Environment]::NewLine) + } + $metadata | ConvertTo-Json -Depth 4 | + Set-Content -LiteralPath './artifacts/benchmark-report/run-metadata.json' -Encoding utf8 - name: Compare with checked-in baseline shell: pwsh run: | - $result = Get-ChildItem './.tmp/benchmarks/BenchmarkDotNet.Artifacts/results/*-report-full-compressed.json' | + $result = Get-ChildItem './.tmp/benchmarks/BenchmarkDotNet.Artifacts/results/*-report-full.json' | Select-Object -First 1 if ($null -eq $result) { throw 'BenchmarkDotNet JSON output was not produced.' diff --git a/CHANGELOG.md b/CHANGELOG.md index dba14ea..4ddafca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,10 @@ The historical archived package history is not reconstructed here. This changelo ### Changed -- Generated materializers now accept safe additional unmapped reader columns and provide specific strict diagnostics for missing, duplicate/ambiguous and mapped additional columns. +- Generated materializers now accept safe additional reader columns that do not resolve to explicit FluentMap members and provide specific strict diagnostics for missing, duplicate/ambiguous and explicitly mapped additional columns; Dapper convention-only discovery remains a non-strict behavior. - Two- and three-input multi-mapping now share a single segment materialization pipeline. - Public projects now emit deliberate nullable reference annotations using staged `Nullable=annotations`; optional query inputs, metadata and multi-mapping null-child delegates reflect actual behavior without changing CLR signatures. +- Generated value-conversion failures now surface as contextual `FluentMapConfigurationException` instances instead of leaking provider-dependent `FormatException` or `InvalidCastException` exceptions. ## [3.4.0] - 2026-09-27 diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 32ef73c..429b353 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -88,7 +88,7 @@ Current status: Trimmed smoke tests cover explicit, generated and DI scenarios. The CI Native AOT lane publishes and runs the strict generated SQLite smoke on `windows-latest`/`win-x64`, treating relevant trimming and AOT warnings as errors. Local Native AOT publishing still requires the Visual C++ linker toolchain and full Native AOT compatibility is not claimed. -Generated materializers accept their exact registered shape, safe permutations and additional reader columns that do not resolve to mapped members. Missing required columns, duplicate/ambiguous names and additional columns that would map to an entity member remain unsupported and use runtime fallback unless strict generated materialization is enabled. Resolution is based on reader metadata, not SQL parsing. +Generated materializers accept their exact registered shape, safe permutations and additional reader columns that do not resolve to explicitly configured FluentMap members. Missing required columns, duplicate/ambiguous names and additional columns that would resolve to an explicit FluentMap member remain unsupported and use runtime fallback unless strict generated materialization is enabled. The strict path deliberately does not reflect over convention-only entity members; callers that need Dapper's default-member discovery must use the non-strict API. Resolution is based on reader metadata, not SQL parsing. `GeneratedParameters` uses only public ADO.NET parameter contracts and requires an explicit `DbType` for every input. It does not inspect parameter-object members or invoke Dapper's anonymous-object parameter generation. The Native AOT smoke covers generated startup registration, parameter binding, reordered columns, a safe additional column and deterministic strict rejection. This is bounded evidence for the strict path, not a claim of full FluentMap Native AOT compatibility. diff --git a/MAINTAINING.md b/MAINTAINING.md index 953b785..854ec11 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -9,7 +9,7 @@ Every shipped project under `src/` has `PublicAPI.Shipped.txt` and `PublicAPI.Un For an intentional API change: 1. update implementation, XML documentation and focused tests; -2. run `dotnet format analyzers --diagnostics RS0016 RS0017` to update `PublicAPI.Unshipped.txt`; +2. run `dotnet format analyzers --diagnostics RS0016 RS0036` to update additions and nullable annotations in `PublicAPI.Unshipped.txt`; record approved removals of shipped APIs manually in `PublicAPI.Unshipped.txt` with the `*REMOVED*` prefix; 3. review the API-file diff as a compatibility contract, including nullability markers; 4. document consumer and SemVer impact; 5. after the API ships, move its entries from `PublicAPI.Unshipped.txt` to `PublicAPI.Shipped.txt` in ordinal order. diff --git a/MIGRATION.md b/MIGRATION.md index 1a248c9..7544706 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -298,7 +298,7 @@ For value objects stored through mapped components, use FluentMap-controlled mat Map(customer => customer.Cpf.Number).ToColumn("cpf"); ``` -The runtime materializer still selects compatible public constructors by default. Maps that previously could not be materialized can now opt into `ConstructUsing(...)` with one to four explicitly mapped values. This factory path is supported by runtime materialization and isolated runtimes; generated materializers report `DFM011`, and strict generated mode rejects the unsupported shape without fallback. +The runtime materializer still selects compatible public constructors by default. Maps that previously could not be materialized can now opt into `ConstructUsing(...)` with one to four explicitly mapped root-property values; nested paths are rejected during configuration. This factory path is supported by runtime materialization and isolated runtimes; generated materializers report `DFM011`, and strict generated mode rejects the unsupported shape without fallback. ## Profiles @@ -342,10 +342,11 @@ Configured write converters execute only through the Dommel adapter's opt-in `In Three-input `QueryMapped` overloads now complement the historical two-input API. Supply two comma-separated unique `splitOn` boundaries. These APIs compose individual rows only and do not aggregate one-to-many graphs. -## Generated Registration +## Nullable Annotations Public assemblies now carry nullable reference annotations. CLR signatures and binary compatibility are unchanged, but nullable-enabled consumers may receive more accurate compiler warnings: optional query parameters/transactions and optional metadata are nullable, and multi-mapping child arguments are nullable because an all-`NULL` segment is passed as `null`. Treat new warnings as contract guidance; do not disable nullable analysis globally to hide them. +## Generated Registration Install `FluentMap.Generators` and call: @@ -357,7 +358,9 @@ This can replace manual registration for eligible maps in the current compilatio Generated materializers are an optimization. Unsupported cases fall back to runtime materialization unless strict generated materialization is explicitly enabled. -Strict generated queries can use `GeneratedParameters` for parameterized commands. Each value has an explicit `DbType`; anonymous objects and other arbitrary parameter bags remain unsupported so strict mode does not introduce reflection-based parameter discovery. Safe reordered and unmapped additional result columns can stay on the generated path, while missing, duplicate/ambiguous or mapped additional columns fail deterministically. +Strict generated queries can use `GeneratedParameters` for parameterized commands. Each value has an explicit `DbType`; anonymous objects and other arbitrary parameter bags remain unsupported so strict mode does not introduce reflection-based parameter discovery. Safe reordered and additional result columns that do not resolve to explicitly configured FluentMap members can stay on the generated path, while missing, duplicate/ambiguous or explicitly mapped additional columns fail deterministically. Convention-only Dapper member discovery remains exclusive to the non-strict path. + +Generated value-conversion failures now throw `FluentMapConfigurationException` with entity, member, column, provider type and target type context. Code that previously caught a provider-dependent `FormatException` or `InvalidCastException` from generated materialization should catch `FluentMapConfigurationException` and inspect its inner exception instead. ## Configuration Isolation diff --git a/MIGRATION.pt-BR.md b/MIGRATION.pt-BR.md index b79fe5b..1eb20da 100644 --- a/MIGRATION.pt-BR.md +++ b/MIGRATION.pt-BR.md @@ -298,7 +298,7 @@ Para value objects armazenados por componentes mapeados, use materialização co Map(customer => customer.Cpf.Number).ToColumn("cpf"); ``` -O materializador de runtime continua selecionando construtores públicos compatíveis por padrão. Maps que antes não podiam ser materializados agora podem optar por `ConstructUsing(...)`, com um a quatro valores explicitamente mapeados. Esse caminho de factory é suportado pela materialização em runtime e por runtimes isolados; materializadores gerados reportam `DFM011`, e o modo strict generated rejeita o shape sem fallback. +O materializador de runtime continua selecionando construtores públicos compatíveis por padrão. Maps que antes não podiam ser materializados agora podem optar por `ConstructUsing(...)`, com um a quatro valores de propriedades raiz explicitamente mapeadas; caminhos aninhados são rejeitados durante a configuração. Esse caminho de factory é suportado pela materialização em runtime e por runtimes isolados; materializadores gerados reportam `DFM011`, e o modo strict generated rejeita o shape sem fallback. ## Profiles @@ -342,10 +342,11 @@ Write converters configurados são executados apenas pelos métodos opt-in `Inse Overloads `QueryMapped` com três entradas agora complementam a API histórica de duas entradas. Informe duas fronteiras `splitOn` únicas, separadas por vírgula. Essas APIs apenas compõem linhas individuais e não agregam grafos um-para-muitos. -## Registro Gerado +## Anotações Nullable Os assemblies públicos agora carregam anotações de nullable reference types. As assinaturas CLR e a compatibilidade binária não mudam, mas consumidores com nullable habilitado podem receber warnings mais precisos: parâmetros/transações opcionais e metadados opcionais são nullable, e argumentos filhos de multi-mapping são nullable porque um segmento contendo apenas `NULL` é entregue como `null`. Trate os novos warnings como orientação de contrato; não desabilite a análise nullable globalmente para ocultá-los. +## Registro Gerado Instale `FluentMap.Generators` e chame: @@ -357,7 +358,9 @@ Isso pode substituir registro manual para maps elegíveis da compilação atual. 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. -Queries strict generated podem usar `GeneratedParameters` para comandos parametrizados. Cada valor possui um `DbType` explícito; objetos anônimos e outros bags de parâmetros arbitrários continuam não suportados, para que o modo estrito não introduza descoberta de parâmetros por reflection. Colunas reordenadas e colunas adicionais não mapeadas seguras podem permanecer no caminho gerado; colunas obrigatórias ausentes, nomes duplicados/ambíguos ou colunas adicionais mapeadas falham deterministicamente. +Queries strict generated podem usar `GeneratedParameters` para comandos parametrizados. Cada valor possui um `DbType` explícito; objetos anônimos e outros bags de parâmetros arbitrários continuam não suportados, para que o modo estrito não introduza descoberta de parâmetros por reflection. Colunas reordenadas e colunas adicionais que não resolvem para membros configurados explicitamente no FluentMap podem permanecer no caminho gerado; colunas obrigatórias ausentes, nomes duplicados/ambíguos ou colunas adicionais explicitamente mapeadas falham deterministicamente. A descoberta de membros apenas por convenção do Dapper permanece exclusiva do caminho não estrito. + +Falhas de conversão de valores gerados agora lançam `FluentMapConfigurationException` com contexto da entidade, membro, coluna, tipo do provider e tipo de destino. Código que antes capturava `FormatException` ou `InvalidCastException`, dependentes do provider, deve capturar `FluentMapConfigurationException` e inspecionar a exceção interna. ## Isolamento de Configuração diff --git a/USAGE.md b/USAGE.md index 84d2ee1..3e5b1c5 100644 --- a/USAGE.md +++ b/USAGE.md @@ -156,7 +156,7 @@ public CustomerMap() } ``` -`ConstructUsing` supports one to four explicitly mapped values, validates every binding, and is preserved by isolated runtimes. Delegate factories are runtime-materialization strategies: the source generator registers the map but reports `DFM011` and does not emit a generated materializer. Strict generated mode therefore rejects that shape instead of silently using the factory through reflection. +`ConstructUsing` supports one to four explicitly mapped root-property values, validates every binding, and is preserved by isolated runtimes. Nested property paths are rejected during configuration because explicit factories currently bind only the root materialization node. Delegate factories are runtime-materialization strategies: the source generator registers the map but reports `DFM011` and does not emit a generated materializer. Strict generated mode therefore rejects that shape instead of silently using the factory through reflection. ## Mapping Profiles @@ -412,7 +412,7 @@ var customer = runtime.QueryGeneratedMappedSingle( `GeneratedParameters` binds only explicitly named input values with an explicit `DbType`; it does not inspect anonymous objects. Parameterless commands remain supported. Arbitrary parameter objects fail before the connection is opened, with `QueryMapped*` identified as the non-strict alternative. -Generated materializers accept reordered required columns and additional columns only when the additional names do not resolve to mapped members. Missing required columns, duplicate names, mapped additional columns, incompatible provider values and unsupported nested shapes fail deterministically instead of silently using runtime materialization. +Generated materializers accept reordered required columns and additional columns only when the additional names do not resolve to explicitly configured FluentMap members. Missing required columns, duplicate names, explicitly mapped additional columns, incompatible provider values and unsupported nested shapes fail deterministically instead of silently using runtime materialization. Strict generated resolution does not reflect over convention-only entity members; use the non-strict API when Dapper default-member discovery is required. 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. diff --git a/USAGE.pt-BR.md b/USAGE.pt-BR.md index 4498211..72aabc8 100644 --- a/USAGE.pt-BR.md +++ b/USAGE.pt-BR.md @@ -156,7 +156,7 @@ public CustomerMap() } ``` -`ConstructUsing` aceita de um a quatro valores explicitamente mapeados, valida cada binding e é preservado por runtimes isolados. Factories delegate são estratégias de materialização em runtime: o source generator registra o map, mas reporta `DFM011` e não emite materializador gerado. Portanto, o modo strict generated rejeita esse shape sem fallback silencioso. +`ConstructUsing` aceita de um a quatro valores de propriedades raiz explicitamente mapeadas, valida cada binding e é preservado por runtimes isolados. Caminhos de propriedades aninhadas são rejeitados durante a configuração porque factories explícitas atualmente vinculam apenas o nó raiz de materialização. Factories delegate são estratégias de materialização em runtime: o source generator registra o map, mas reporta `DFM011` e não emite materializador gerado. Portanto, o modo strict generated rejeita esse shape sem fallback silencioso. ## Mapping Profiles @@ -412,7 +412,7 @@ var customer = runtime.QueryGeneratedMappedSingle( `GeneratedParameters` vincula somente valores de entrada nomeados explicitamente e com `DbType` explícito; ele não inspeciona objetos anônimos. Comandos sem parâmetros continuam suportados. Objetos de parâmetros arbitrários falham antes de abrir a conexão, e o diagnóstico identifica `QueryMapped*` como alternativa não estrita. -Materializadores gerados aceitam colunas obrigatórias reordenadas e colunas adicionais somente quando os nomes adicionais não resolvem membros mapeados. Colunas obrigatórias ausentes, nomes duplicados, colunas adicionais mapeadas, valores CLR incompatíveis do provider e shapes aninhados não suportados falham deterministicamente, sem fallback silencioso para materialização runtime. +Materializadores gerados aceitam colunas obrigatórias reordenadas e colunas adicionais somente quando os nomes adicionais não resolvem membros configurados explicitamente no FluentMap. Colunas obrigatórias ausentes, nomes duplicados, colunas adicionais explicitamente mapeadas, valores CLR incompatíveis do provider e shapes aninhados não suportados falham deterministicamente, sem fallback silencioso para materialização runtime. A resolução strict generated não usa reflection para descobrir membros disponíveis apenas pela convenção do Dapper; use a API não estrita quando essa descoberta for necessária. 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. diff --git a/eng/compare-benchmark-results.ps1 b/eng/compare-benchmark-results.ps1 index be573b1..2a903ff 100644 --- a/eng/compare-benchmark-results.ps1 +++ b/eng/compare-benchmark-results.ps1 @@ -15,16 +15,26 @@ $ErrorActionPreference = 'Stop' $baseline = Get-Content -Raw -LiteralPath $BaselinePath | ConvertFrom-Json $current = Get-Content -Raw -LiteralPath $CurrentPath | ConvertFrom-Json $shortRun = @($current.Benchmarks | Where-Object { $_.DisplayInfo -like '*ShortRun*' }) +if ($shortRun.Count -eq 0) { + throw 'The BenchmarkDotNet report contains no ShortRun benchmark results.' +} New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null $comparisons = foreach ($scenario in $baseline.scenarios) { - $matches = @($shortRun | Where-Object { $_.Method -eq $scenario.method }) - if ($matches.Count -ne 1) { - throw "Expected one ShortRun result for '$($scenario.method)', found $($matches.Count)." + $candidates = @($shortRun | Where-Object { $_.Method -eq $scenario.method }) + if ($candidates.Count -ne 1) { + throw "Expected one ShortRun result for '$($scenario.method)', found $($candidates.Count)." + } + + $result = $candidates[0] + if ($null -eq $result.Statistics -or + $null -eq $result.Statistics.Mean -or + $null -eq $result.Memory -or + $null -eq $result.Memory.BytesAllocatedPerOperation) { + throw "Benchmark result for '$($scenario.method)' is missing Statistics.Mean or Memory.BytesAllocatedPerOperation." } - $result = $matches[0] $timeDelta = (($result.Statistics.Mean - $scenario.meanNanoseconds) / $scenario.meanNanoseconds) * 100 $allocationDelta = (($result.Memory.BytesAllocatedPerOperation - $scenario.allocatedBytes) / $scenario.allocatedBytes) * 100 $material = $timeDelta -gt $baseline.timeRegressionThresholdPercent -or diff --git a/src/Dapper.FluentMap.Analyzers/FluentMapConfigurationAnalyzer.cs b/src/Dapper.FluentMap.Analyzers/FluentMapConfigurationAnalyzer.cs index b5c82d9..25a1148 100644 --- a/src/Dapper.FluentMap.Analyzers/FluentMapConfigurationAnalyzer.cs +++ b/src/Dapper.FluentMap.Analyzers/FluentMapConfigurationAnalyzer.cs @@ -204,7 +204,7 @@ private static void AnalyzeInvocation( if (constructor?.MethodKind == MethodKind.Constructor) { constructionStrategies.Add(new ConstructionStrategyInvocation( - constructor, + constructor.ContainingType, GetInvocationNameLocation(invocation))); } @@ -386,17 +386,40 @@ private static void AnalyzeConstructionStrategies( CompilationAnalysisContext context, ConcurrentBag constructionStrategies) { - foreach (var group in constructionStrategies.GroupBy( - invocation => invocation.Constructor, - SymbolEqualityComparer.Default)) + var invocations = constructionStrategies + .OrderBy(invocation => invocation.Location.SourceSpan.Start) + .ToList(); + + for (var index = 0; index < invocations.Count; index++) { - foreach (var duplicate in group.OrderBy(invocation => invocation.Location.SourceSpan.Start).Skip(1)) + var candidate = invocations[index]; + var duplicatesSameMap = invocations + .Take(index) + .Any(previous => SymbolEqualityComparer.Default.Equals(previous.MapType, candidate.MapType)); + var duplicatesBaseMap = invocations.Any(other => + !ReferenceEquals(other, candidate) && + IsDerivedFrom(candidate.MapType, other.MapType)); + + if (duplicatesSameMap || duplicatesBaseMap) { context.ReportDiagnostic(Diagnostic.Create( DuplicateConstructionStrategyRule, - duplicate.Location)); + candidate.Location)); + } + } + } + + private static bool IsDerivedFrom(INamedTypeSymbol type, INamedTypeSymbol possibleBaseType) + { + for (var current = type.BaseType; current != null; current = current.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(current, possibleBaseType)) + { + return true; } } + + return false; } private static void ReportDuplicateMemberPaths( @@ -1458,13 +1481,13 @@ internal ProfileRegistrationInvocation( private sealed class ConstructionStrategyInvocation { - internal ConstructionStrategyInvocation(IMethodSymbol constructor, Location location) + internal ConstructionStrategyInvocation(INamedTypeSymbol mapType, Location location) { - Constructor = constructor; + MapType = mapType; Location = location; } - internal IMethodSymbol Constructor { get; } + internal INamedTypeSymbol MapType { get; } internal Location Location { get; } } diff --git a/src/Dapper.FluentMap.Analyzers/PublicAPI.Shipped.txt b/src/Dapper.FluentMap.Analyzers/PublicAPI.Shipped.txt index 7dc5c58..93ba1b6 100644 --- a/src/Dapper.FluentMap.Analyzers/PublicAPI.Shipped.txt +++ b/src/Dapper.FluentMap.Analyzers/PublicAPI.Shipped.txt @@ -1 +1,16 @@ #nullable enable + +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicateColumnDiagnosticId = "DFM003" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicateMemberPathDiagnosticId = "DFM002" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicateProfileRegistrationDiagnosticId = "DFM010" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicatePropertyConverterDiagnosticId = "DFM015" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidGenericMapRegistrationDiagnosticId = "DFM005" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidGenericProfileRegistrationDiagnosticId = "DFM009" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidIncludeBaseDiagnosticId = "DFM004" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidMapExpressionDiagnosticId = "DFM001" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidPersistenceBehaviorDiagnosticId = "DFM013" -> string! +const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidPropertyConverterDiagnosticId = "DFM014" -> string! +Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer +Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.FluentMapConfigurationAnalyzer() -> void +override Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext! context) -> void +override Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.SupportedDiagnostics.get -> System.Collections.Immutable.ImmutableArray diff --git a/src/Dapper.FluentMap.Analyzers/PublicAPI.Unshipped.txt b/src/Dapper.FluentMap.Analyzers/PublicAPI.Unshipped.txt index 3004644..2256098 100644 --- a/src/Dapper.FluentMap.Analyzers/PublicAPI.Unshipped.txt +++ b/src/Dapper.FluentMap.Analyzers/PublicAPI.Unshipped.txt @@ -1,16 +1,2 @@ #nullable enable -const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicateColumnDiagnosticId = "DFM003" -> string! const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicateConstructionStrategyDiagnosticId = "DFM016" -> string! -const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicateMemberPathDiagnosticId = "DFM002" -> string! -const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicateProfileRegistrationDiagnosticId = "DFM010" -> string! -const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.DuplicatePropertyConverterDiagnosticId = "DFM015" -> string! -const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidGenericMapRegistrationDiagnosticId = "DFM005" -> string! -const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidGenericProfileRegistrationDiagnosticId = "DFM009" -> string! -const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidIncludeBaseDiagnosticId = "DFM004" -> string! -const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidMapExpressionDiagnosticId = "DFM001" -> string! -const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidPersistenceBehaviorDiagnosticId = "DFM013" -> string! -const Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.InvalidPropertyConverterDiagnosticId = "DFM014" -> string! -Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer -Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.FluentMapConfigurationAnalyzer() -> void -override Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext! context) -> void -override Dapper.FluentMap.Analyzers.FluentMapConfigurationAnalyzer.SupportedDiagnostics.get -> System.Collections.Immutable.ImmutableArray diff --git a/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Shipped.txt b/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Shipped.txt index 7dc5c58..4a3345c 100644 --- a/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Shipped.txt +++ b/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Shipped.txt @@ -1 +1,4 @@ #nullable enable + +Microsoft.Extensions.DependencyInjection.FluentMapServiceCollectionExtensions +static Microsoft.Extensions.DependencyInjection.FluentMapServiceCollectionExtensions.AddFluentMap(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Unshipped.txt b/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Unshipped.txt index 283dd36..7dc5c58 100644 --- a/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Unshipped.txt +++ b/src/Dapper.FluentMap.DependencyInjection/PublicAPI.Unshipped.txt @@ -1,3 +1 @@ #nullable enable -Microsoft.Extensions.DependencyInjection.FluentMapServiceCollectionExtensions -static Microsoft.Extensions.DependencyInjection.FluentMapServiceCollectionExtensions.AddFluentMap(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs b/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs index d149a82..69ef61e 100644 --- a/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs +++ b/src/Dapper.FluentMap.Dommel/DommelWriteExtensions.cs @@ -100,6 +100,12 @@ private static CommandDefinition CreateInsertCommand( new DommelPropertyResolver().ResolveProperties(type).Select(property => property.Property)) .Where(property => !generatedKeys.Contains(property)) .ToArray(); + if (properties.Length == 0) + { + throw new FluentMapConfigurationException( + $"Entity '{type.FullName}' has no properties eligible for a mapped INSERT."); + } + var tableName = global::Dommel.Resolvers.Table(type, sqlBuilder); var columnNames = properties .Select(property => global::Dommel.Resolvers.Column(property, sqlBuilder, false)) @@ -126,12 +132,24 @@ private static CommandDefinition CreateUpdateCommand( var type = typeof(TEntity); var sqlBuilder = DommelMapper.GetSqlBuilder(connection); var keyProperties = new DommelKeyPropertyResolver().ResolveKeyProperties(type); + if (keyProperties.Length == 0) + { + throw new FluentMapConfigurationException( + $"Entity '{type.FullName}' has no key property available for a mapped UPDATE predicate."); + } + var keySet = new HashSet(keyProperties.Select(key => key.Property)); var updateProperties = new DommelPropertyResolver() .ResolveProperties(type) .Where(property => !property.IsGenerated && !keySet.Contains(property.Property)) .Select(property => property.Property) .ToArray(); + if (updateProperties.Length == 0) + { + throw new FluentMapConfigurationException( + $"Entity '{type.FullName}' has no properties eligible for a mapped UPDATE."); + } + var tableName = global::Dommel.Resolvers.Table(type, sqlBuilder); var assignments = updateProperties.Select(property => global::Dommel.Resolvers.Column(property, sqlBuilder, false) + " = " + @@ -166,7 +184,9 @@ private static DynamicParameters CreateParameters( var conversion = propertyMap == null ? PropertyConversionMetadata.Default : PropertyMapConversion.GetConversion(propertyMap); - var columnName = propertyMap == null ? property.Name : propertyMap.ColumnName; + var columnName = propertyMap == null || string.IsNullOrWhiteSpace(propertyMap.ColumnName) + ? property.Name + : propertyMap.ColumnName; if (conversion.HasWriteConverter && value != null) { diff --git a/src/Dapper.FluentMap.Dommel/PublicAPI.Shipped.txt b/src/Dapper.FluentMap.Dommel/PublicAPI.Shipped.txt index 7dc5c58..cb6955e 100644 --- a/src/Dapper.FluentMap.Dommel/PublicAPI.Shipped.txt +++ b/src/Dapper.FluentMap.Dommel/PublicAPI.Shipped.txt @@ -1 +1,35 @@ #nullable enable + +Dapper.FluentMap.Dommel.FluentMapConfigurationExtensions +Dapper.FluentMap.Dommel.Mapping.DommelEntityMap +Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.DommelEntityMap() -> void +Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.TableName.get -> string! +Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.ToTable(string! tableName) -> void +Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.ToTable(string! tableName, string! schemaName) -> void +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.DommelPropertyMap(System.Reflection.PropertyInfo! info) -> void +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.GeneratedOption.get -> System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption? +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.GeneratedOption.set -> void +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.Identity.get -> bool +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.Identity.set -> void +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.IsIdentity() -> Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap! +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.IsKey() -> Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap! +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.Key.get -> bool +Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.SetGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption option) -> Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap! +Dapper.FluentMap.Dommel.Mapping.IDommelEntityMap +Dapper.FluentMap.Dommel.Mapping.IDommelEntityMap.TableName.get -> string! +Dapper.FluentMap.Dommel.Resolvers.DommelColumnNameResolver +Dapper.FluentMap.Dommel.Resolvers.DommelColumnNameResolver.DommelColumnNameResolver() -> void +Dapper.FluentMap.Dommel.Resolvers.DommelColumnNameResolver.ResolveColumnName(System.Reflection.PropertyInfo! propertyInfo) -> string! +Dapper.FluentMap.Dommel.Resolvers.DommelKeyPropertyResolver +Dapper.FluentMap.Dommel.Resolvers.DommelKeyPropertyResolver.DommelKeyPropertyResolver() -> void +Dapper.FluentMap.Dommel.Resolvers.DommelKeyPropertyResolver.ResolveKeyProperties(System.Type! type) -> Dommel.ColumnPropertyInfo![]! +Dapper.FluentMap.Dommel.Resolvers.DommelPropertyResolver +Dapper.FluentMap.Dommel.Resolvers.DommelPropertyResolver.DommelPropertyResolver() -> void +Dapper.FluentMap.Dommel.Resolvers.DommelTableNameResolver +Dapper.FluentMap.Dommel.Resolvers.DommelTableNameResolver.DommelTableNameResolver() -> void +Dapper.FluentMap.Dommel.Resolvers.DommelTableNameResolver.ResolveTableName(System.Type! type) -> string! +override Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.GetPropertyMap(System.Reflection.PropertyInfo! info) -> Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap! +override Dapper.FluentMap.Dommel.Resolvers.DommelPropertyResolver.FilterComplexTypes(System.Collections.Generic.IEnumerable! properties) -> System.Collections.Generic.IEnumerable! +override Dapper.FluentMap.Dommel.Resolvers.DommelPropertyResolver.ResolveProperties(System.Type! type) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.Dommel.FluentMapConfigurationExtensions.ForDommel(this Dapper.FluentMap.Configuration.FluentMapConfiguration! config) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! diff --git a/src/Dapper.FluentMap.Dommel/PublicAPI.Unshipped.txt b/src/Dapper.FluentMap.Dommel/PublicAPI.Unshipped.txt index d09b0d6..9743831 100644 --- a/src/Dapper.FluentMap.Dommel/PublicAPI.Unshipped.txt +++ b/src/Dapper.FluentMap.Dommel/PublicAPI.Unshipped.txt @@ -1,39 +1,6 @@ #nullable enable Dapper.FluentMap.Dommel.DommelWriteExtensions -Dapper.FluentMap.Dommel.FluentMapConfigurationExtensions -Dapper.FluentMap.Dommel.Mapping.DommelEntityMap -Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.DommelEntityMap() -> void -Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.TableName.get -> string! -Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.ToTable(string! tableName) -> void -Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.ToTable(string! tableName, string! schemaName) -> void -Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap -Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.DommelPropertyMap(System.Reflection.PropertyInfo! info) -> void -Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.GeneratedOption.get -> System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption? -Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.GeneratedOption.set -> void -Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.Identity.get -> bool -Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.Identity.set -> void -Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.IsIdentity() -> Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap! -Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.IsKey() -> Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap! -Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.Key.get -> bool -Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap.SetGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption option) -> Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap! -Dapper.FluentMap.Dommel.Mapping.IDommelEntityMap -Dapper.FluentMap.Dommel.Mapping.IDommelEntityMap.TableName.get -> string! -Dapper.FluentMap.Dommel.Resolvers.DommelColumnNameResolver -Dapper.FluentMap.Dommel.Resolvers.DommelColumnNameResolver.DommelColumnNameResolver() -> void -Dapper.FluentMap.Dommel.Resolvers.DommelColumnNameResolver.ResolveColumnName(System.Reflection.PropertyInfo! propertyInfo) -> string! -Dapper.FluentMap.Dommel.Resolvers.DommelKeyPropertyResolver -Dapper.FluentMap.Dommel.Resolvers.DommelKeyPropertyResolver.DommelKeyPropertyResolver() -> void -Dapper.FluentMap.Dommel.Resolvers.DommelKeyPropertyResolver.ResolveKeyProperties(System.Type! type) -> Dommel.ColumnPropertyInfo![]! -Dapper.FluentMap.Dommel.Resolvers.DommelPropertyResolver -Dapper.FluentMap.Dommel.Resolvers.DommelPropertyResolver.DommelPropertyResolver() -> void -Dapper.FluentMap.Dommel.Resolvers.DommelTableNameResolver -Dapper.FluentMap.Dommel.Resolvers.DommelTableNameResolver.DommelTableNameResolver() -> void -Dapper.FluentMap.Dommel.Resolvers.DommelTableNameResolver.ResolveTableName(System.Type! type) -> string! -override Dapper.FluentMap.Dommel.Mapping.DommelEntityMap.GetPropertyMap(System.Reflection.PropertyInfo! info) -> Dapper.FluentMap.Dommel.Mapping.DommelPropertyMap! -override Dapper.FluentMap.Dommel.Resolvers.DommelPropertyResolver.FilterComplexTypes(System.Collections.Generic.IEnumerable! properties) -> System.Collections.Generic.IEnumerable! -override Dapper.FluentMap.Dommel.Resolvers.DommelPropertyResolver.ResolveProperties(System.Type! type) -> System.Collections.Generic.IEnumerable! static Dapper.FluentMap.Dommel.DommelWriteExtensions.InsertMapped(this System.Data.IDbConnection! connection, TEntity! entity, System.Data.IDbTransaction? transaction = null) -> object? static Dapper.FluentMap.Dommel.DommelWriteExtensions.InsertMappedAsync(this System.Data.IDbConnection! connection, TEntity! entity, System.Data.IDbTransaction? transaction = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! static Dapper.FluentMap.Dommel.DommelWriteExtensions.UpdateMapped(this System.Data.IDbConnection! connection, TEntity! entity, System.Data.IDbTransaction? transaction = null) -> bool static Dapper.FluentMap.Dommel.DommelWriteExtensions.UpdateMappedAsync(this System.Data.IDbConnection! connection, TEntity! entity, System.Data.IDbTransaction? transaction = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Dapper.FluentMap.Dommel.FluentMapConfigurationExtensions.ForDommel(this Dapper.FluentMap.Configuration.FluentMapConfiguration! config) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! diff --git a/src/Dapper.FluentMap.Generators/MappingRegistrationGenerator.cs b/src/Dapper.FluentMap.Generators/MappingRegistrationGenerator.cs index dfb59ac..900183e 100644 --- a/src/Dapper.FluentMap.Generators/MappingRegistrationGenerator.cs +++ b/src/Dapper.FluentMap.Generators/MappingRegistrationGenerator.cs @@ -861,7 +861,7 @@ private static void AppendReadHelper(StringBuilder builder) builder.AppendLine(" throw new global::Dapper.FluentMap.FluentMapConfigurationException("); builder.AppendLine(" \"Generated materialization failed for entity '\" + entityTypeName + \"'.\" + profileContext +"); builder.AppendLine(" \" Member path: '\" + memberPath + \"'. Column: '\" + columnName + \"'. Provider CLR type: '\" + value.GetType().FullName +"); - builder.AppendLine(" \"'. Target type: '\" + targetTypeName + \"'. Strict generated materialization does not use runtime reflection fallback.\","); + builder.AppendLine(" \"'. Target type: '\" + targetTypeName + \"'. Generated materializers do not fall back to runtime reflection conversion.\","); builder.AppendLine(" exception);"); builder.AppendLine(" }"); builder.AppendLine(" }"); diff --git a/src/Dapper.FluentMap.Generators/PublicAPI.Shipped.txt b/src/Dapper.FluentMap.Generators/PublicAPI.Shipped.txt index 7dc5c58..0f44983 100644 --- a/src/Dapper.FluentMap.Generators/PublicAPI.Shipped.txt +++ b/src/Dapper.FluentMap.Generators/PublicAPI.Shipped.txt @@ -1 +1,11 @@ #nullable enable + +const Dapper.FluentMap.Generators.MappingRegistrationGenerator.DuplicateGeneratedEntityMapDiagnosticId = "DFM007" -> string! +const Dapper.FluentMap.Generators.MappingRegistrationGenerator.DuplicateGeneratedProfileMapDiagnosticId = "DFM008" -> string! +const Dapper.FluentMap.Generators.MappingRegistrationGenerator.InvalidGeneratedReadConverterDiagnosticId = "DFM012" -> string! +const Dapper.FluentMap.Generators.MappingRegistrationGenerator.InvalidGenericMapRegistrationDiagnosticId = "DFM005" -> string! +const Dapper.FluentMap.Generators.MappingRegistrationGenerator.SkippedGeneratedMapDiagnosticId = "DFM006" -> string! +const Dapper.FluentMap.Generators.MappingRegistrationGenerator.SkippedGeneratedMaterializerDiagnosticId = "DFM011" -> string! +Dapper.FluentMap.Generators.MappingRegistrationGenerator +Dapper.FluentMap.Generators.MappingRegistrationGenerator.Initialize(Microsoft.CodeAnalysis.IncrementalGeneratorInitializationContext context) -> void +Dapper.FluentMap.Generators.MappingRegistrationGenerator.MappingRegistrationGenerator() -> void diff --git a/src/Dapper.FluentMap.Generators/PublicAPI.Unshipped.txt b/src/Dapper.FluentMap.Generators/PublicAPI.Unshipped.txt index f6f9b23..7dc5c58 100644 --- a/src/Dapper.FluentMap.Generators/PublicAPI.Unshipped.txt +++ b/src/Dapper.FluentMap.Generators/PublicAPI.Unshipped.txt @@ -1,10 +1 @@ #nullable enable -const Dapper.FluentMap.Generators.MappingRegistrationGenerator.DuplicateGeneratedEntityMapDiagnosticId = "DFM007" -> string! -const Dapper.FluentMap.Generators.MappingRegistrationGenerator.DuplicateGeneratedProfileMapDiagnosticId = "DFM008" -> string! -const Dapper.FluentMap.Generators.MappingRegistrationGenerator.InvalidGeneratedReadConverterDiagnosticId = "DFM012" -> string! -const Dapper.FluentMap.Generators.MappingRegistrationGenerator.InvalidGenericMapRegistrationDiagnosticId = "DFM005" -> string! -const Dapper.FluentMap.Generators.MappingRegistrationGenerator.SkippedGeneratedMapDiagnosticId = "DFM006" -> string! -const Dapper.FluentMap.Generators.MappingRegistrationGenerator.SkippedGeneratedMaterializerDiagnosticId = "DFM011" -> string! -Dapper.FluentMap.Generators.MappingRegistrationGenerator -Dapper.FluentMap.Generators.MappingRegistrationGenerator.Initialize(Microsoft.CodeAnalysis.IncrementalGeneratorInitializationContext context) -> void -Dapper.FluentMap.Generators.MappingRegistrationGenerator.MappingRegistrationGenerator() -> void diff --git a/src/Dapper.FluentMap/Configuration/ImmutableFluentMapConfiguration.cs b/src/Dapper.FluentMap/Configuration/ImmutableFluentMapConfiguration.cs index fc28fdd..266654e 100644 --- a/src/Dapper.FluentMap/Configuration/ImmutableFluentMapConfiguration.cs +++ b/src/Dapper.FluentMap/Configuration/ImmutableFluentMapConfiguration.cs @@ -209,7 +209,7 @@ private ProfileMappingConfiguration( /// /// Gets the selected mapping profile type. /// - public Type? ProfileType { get; } + public Type ProfileType { get; } /// /// Gets the concrete map type that produced this profile configuration. @@ -387,7 +387,7 @@ public sealed class GeneratedMaterializerConfiguration { private GeneratedMaterializerConfiguration( Type entityType, - Type profileType, + Type? profileType, IReadOnlyList columns, Func materializer) { @@ -405,7 +405,7 @@ private GeneratedMaterializerConfiguration( /// /// Gets the mapping profile type, or for the default map. /// - public Type ProfileType { get; } + public Type? ProfileType { get; } /// /// Gets the ordered column shape expected by the generated materializer. @@ -428,7 +428,7 @@ internal sealed class GeneratedMaterializerRegistrationSnapshot { internal GeneratedMaterializerRegistrationSnapshot( Type entityType, - Type profileType, + Type? profileType, IReadOnlyList columns, Func materializer) { @@ -440,7 +440,7 @@ internal GeneratedMaterializerRegistrationSnapshot( internal Type EntityType { get; } - internal Type ProfileType { get; } + internal Type? ProfileType { get; } internal IReadOnlyList Columns { get; } diff --git a/src/Dapper.FluentMap/FluentMapRuntime.cs b/src/Dapper.FluentMap/FluentMapRuntime.cs index ad1ab64..d4de907 100644 --- a/src/Dapper.FluentMap/FluentMapRuntime.cs +++ b/src/Dapper.FluentMap/FluentMapRuntime.cs @@ -203,11 +203,11 @@ public IEnumerable QueryMapped< IDbConnection connection, string sql, Func map, + string splitOn, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, - CommandType? commandType = null, - string? splitOn = null) + CommandType? commandType = null) where TFirst : class where TSecond : class where TThird : class @@ -241,11 +241,11 @@ public Task> QueryMappedAsync< IDbConnection connection, string sql, Func map, + string splitOn, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, - string? splitOn = null, CancellationToken cancellationToken = default) where TFirst : class where TSecond : class @@ -283,11 +283,11 @@ public Task> QueryMappedAsync< IDbConnection connection, string sql, Func map, + string splitOn, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, - string? splitOn = null, CancellationToken cancellationToken = default) where TFirst : class where TSecond : class @@ -328,11 +328,11 @@ public IEnumerable QueryMapped< IDbConnection connection, string sql, Func map, + string splitOn, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, - CommandType? commandType = null, - string? splitOn = null) + CommandType? commandType = null) where TFirst : class where TSecond : class where TThird : class diff --git a/src/Dapper.FluentMap/MappingConfigurationValidator.cs b/src/Dapper.FluentMap/MappingConfigurationValidator.cs index ab49536..0fb2cfa 100644 --- a/src/Dapper.FluentMap/MappingConfigurationValidator.cs +++ b/src/Dapper.FluentMap/MappingConfigurationValidator.cs @@ -75,6 +75,12 @@ private static void ValidateConstructionStrategy( foreach (var binding in strategy.Bindings) { + if (binding.MemberPath.IsNested) + { + throw new FluentMapConfigurationException( + $"Explicit construction for entity '{FormatType(entityType)}' binds nested property path '{binding.MemberPath}'. ConstructUsing(...) supports only root property paths."); + } + if (!mappedPaths.Contains(binding.MemberPath)) { throw new FluentMapConfigurationException( diff --git a/src/Dapper.FluentMap/MappingRegistry.cs b/src/Dapper.FluentMap/MappingRegistry.cs index 166f542..eae6b29 100644 --- a/src/Dapper.FluentMap/MappingRegistry.cs +++ b/src/Dapper.FluentMap/MappingRegistry.cs @@ -18,6 +18,11 @@ namespace Dapper.FluentMap { internal sealed class MappingRegistry { + private static readonly GeneratedMaterializerEntry NoCompatibleGeneratedMaterializer = + GeneratedMaterializerEntry.Create( + Array.Empty(), + _ => null); + private readonly bool _installDapperTypeMaps; private readonly ConcurrentDictionary _propertyMapCache = @@ -302,6 +307,8 @@ internal void AddGeneratedMaterializer(GeneratedMaterializerDescriptor< throw new FluentMapConfigurationException( $"Entity '{descriptor.EntityType.FullName}' already has a generated materializer registered for the same column shape{profileContext}."); } + + InvalidateType(descriptor.EntityType); } internal void AddGeneratedMaterializer( @@ -340,6 +347,8 @@ internal void AddGeneratedMaterializer( throw new FluentMapConfigurationException( $"Entity '{entityType.FullName}' already has a generated materializer registered for the same column shape{profileContext}."); } + + InvalidateType(entityType); } internal bool TryGetGeneratedMaterializer( @@ -377,6 +386,12 @@ internal bool TryGetGeneratedMaterializer( if (_generatedMaterializerResolutionCache.TryGetValue(cacheKey, out entry)) { + if (ReferenceEquals(entry, NoCompatibleGeneratedMaterializer)) + { + materializer = null; + return false; + } + materializer = entry.Materialize; return true; } @@ -392,6 +407,7 @@ internal bool TryGetGeneratedMaterializer( return true; } + _generatedMaterializerResolutionCache.TryAdd(cacheKey, NoCompatibleGeneratedMaterializer); materializer = null; return false; } diff --git a/src/Dapper.FluentMap/PublicAPI.Shipped.txt b/src/Dapper.FluentMap/PublicAPI.Shipped.txt index 7dc5c58..261b397 100644 --- a/src/Dapper.FluentMap/PublicAPI.Shipped.txt +++ b/src/Dapper.FluentMap/PublicAPI.Shipped.txt @@ -1 +1,362 @@ #nullable enable + +abstract Dapper.FluentMap.Mapping.EntityMapBase.GetPropertyMap(System.Reflection.PropertyInfo! info) -> TPropertyMap +Dapper.FluentMap.Configuration.ConventionMappingConfiguration +Dapper.FluentMap.Configuration.ConventionMappingConfiguration.ConventionType.get -> System.Type! +Dapper.FluentMap.Configuration.ConventionMappingConfiguration.EntityType.get -> System.Type! +Dapper.FluentMap.Configuration.ConventionMappingConfiguration.PropertyMaps.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.EntityMappingConfiguration +Dapper.FluentMap.Configuration.EntityMappingConfiguration.EntityType.get -> System.Type! +Dapper.FluentMap.Configuration.EntityMappingConfiguration.IncludedBaseTypes.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.EntityMappingConfiguration.MapType.get -> System.Type! +Dapper.FluentMap.Configuration.EntityMappingConfiguration.PropertyMaps.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.FluentConventionConfiguration +Dapper.FluentMap.Configuration.FluentConventionConfiguration.FluentConventionConfiguration(Dapper.FluentMap.Conventions.Convention! convention) -> void +Dapper.FluentMap.Configuration.FluentConventionConfiguration.ForEntitiesInAssembly(System.Reflection.Assembly! assembly, params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentConventionConfiguration.ForEntitiesInCurrentAssembly(params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentConventionConfiguration.ForEntity() -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentConventionConfiguration.GetType() -> System.Type! +Dapper.FluentMap.Configuration.FluentMapConfiguration +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddConvention() -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddGeneratedMaterializer(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddGeneratedMaterializer(Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor! descriptor) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddGeneratedMaterializer(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddMap(Dapper.FluentMap.Mapping.IEntityMap! mapper) -> void +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddMap() -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddMapsFromAssembly(System.Reflection.Assembly! assembly, params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddMapsFromAssemblyContaining(params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.AddProfile() -> Dapper.FluentMap.Configuration.FluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.FluentMapConfiguration() -> void +Dapper.FluentMap.Configuration.FluentMapConfiguration.GetType() -> System.Type! +Dapper.FluentMap.Configuration.FluentMapConfiguration.UseNamingPolicy(Dapper.FluentMap.Naming.NamingPolicy! namingPolicy, bool caseSensitive = true) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfiguration.UseNamingPolicy(System.Func! transformer, bool caseSensitive = true) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddConvention() -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddGeneratedMaterializer(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddGeneratedMaterializer(Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor! descriptor) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddGeneratedMaterializer(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddMap(Dapper.FluentMap.Mapping.IEntityMap! mapper) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddMap() -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddMapsFromAssembly(System.Reflection.Assembly! assembly, params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddMapsFromAssemblyContaining(params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddProfile() -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.Build() -> Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.Configure(System.Action! configure) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.FluentMapConfigurationBuilder() -> void +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.UseNamingPolicy(Dapper.FluentMap.Naming.NamingPolicy! namingPolicy, bool caseSensitive = true) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.UseNamingPolicy(System.Func! transformer, bool caseSensitive = true) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.UseStrictGeneratedMaterialization() -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! +Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.Validate() -> void +Dapper.FluentMap.Configuration.GeneratedMaterializerConfiguration +Dapper.FluentMap.Configuration.GeneratedMaterializerConfiguration.Columns.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.GeneratedMaterializerConfiguration.EntityType.get -> System.Type! +Dapper.FluentMap.Configuration.GeneratedMaterializerConfiguration.ProfileType.get -> System.Type? +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.CreateRuntime() -> Dapper.FluentMap.FluentMapRuntime! +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.EntityMaps.get -> System.Collections.Generic.IReadOnlyDictionary! +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.GeneratedMaterializers.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.ProfileMaps.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.StrictGeneratedMaterialization.get -> bool +Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.TypeConventions.get -> System.Collections.Generic.IReadOnlyDictionary!>! +Dapper.FluentMap.Configuration.ProfileMappingConfiguration +Dapper.FluentMap.Configuration.ProfileMappingConfiguration.EntityType.get -> System.Type! +Dapper.FluentMap.Configuration.ProfileMappingConfiguration.IncludedBaseTypes.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.ProfileMappingConfiguration.MapType.get -> System.Type! +Dapper.FluentMap.Configuration.ProfileMappingConfiguration.ProfileType.get -> System.Type! +Dapper.FluentMap.Configuration.ProfileMappingConfiguration.PropertyMaps.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Configuration.PropertyMappingConfiguration +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.CaseSensitive.get -> bool +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.ColumnName.get -> string! +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.Conversion.get -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.Ignored.get -> bool +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.MemberPath.get -> string! +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.Persistence.get -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! +Dapper.FluentMap.Configuration.PropertyMappingConfiguration.PropertyInfo.get -> System.Reflection.PropertyInfo! +Dapper.FluentMap.Conventions.Convention +Dapper.FluentMap.Conventions.Convention.Convention() -> void +Dapper.FluentMap.Conventions.Convention.ConventionConfigurations.get -> System.Collections.Generic.IList! +Dapper.FluentMap.Conventions.Convention.GetType() -> System.Type! +Dapper.FluentMap.Conventions.Convention.Properties() -> Dapper.FluentMap.Conventions.PropertyConventionConfiguration! +Dapper.FluentMap.Conventions.Convention.Properties() -> Dapper.FluentMap.Conventions.PropertyConventionConfiguration! +Dapper.FluentMap.Conventions.Convention.PropertyMaps.get -> System.Collections.Generic.IList! +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.ConventionPropertyConfiguration() -> void +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.GetType() -> System.Type! +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.HasColumnName(string! columnName) -> Dapper.FluentMap.Conventions.ConventionPropertyConfiguration! +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.HasPrefix(string! prefix) -> Dapper.FluentMap.Conventions.ConventionPropertyConfiguration! +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.IsCaseInsensitive() -> Dapper.FluentMap.Conventions.ConventionPropertyConfiguration! +Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.Transform(System.Func! transformer) -> Dapper.FluentMap.Conventions.ConventionPropertyConfiguration! +Dapper.FluentMap.Conventions.PropertyConventionConfiguration +Dapper.FluentMap.Conventions.PropertyConventionConfiguration.Configure(System.Action! configure) -> void +Dapper.FluentMap.Conventions.PropertyConventionConfiguration.GetType() -> System.Type! +Dapper.FluentMap.Conventions.PropertyConventionConfiguration.PropertyConventionConfiguration() -> void +Dapper.FluentMap.Conventions.PropertyConventionConfiguration.Where(System.Func! predicate) -> Dapper.FluentMap.Conventions.PropertyConventionConfiguration! +Dapper.FluentMap.Diagnostics.ConstructorParameterExplanation +Dapper.FluentMap.Diagnostics.ConstructorParameterExplanation.Constructor.get -> System.Reflection.ConstructorInfo! +Dapper.FluentMap.Diagnostics.ConstructorParameterExplanation.Name.get -> string! +Dapper.FluentMap.Diagnostics.ConstructorParameterExplanation.ParameterType.get -> System.Type! +Dapper.FluentMap.Diagnostics.MappingExplanation +Dapper.FluentMap.Diagnostics.MappingExplanation.ConventionTypes.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Diagnostics.MappingExplanation.Diagnostics.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Diagnostics.MappingExplanation.EntityMapType.get -> System.Type? +Dapper.FluentMap.Diagnostics.MappingExplanation.EntityType.get -> System.Type! +Dapper.FluentMap.Diagnostics.MappingExplanation.Members.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Diagnostics.MappingExplanation.ProfileType.get -> System.Type? +Dapper.FluentMap.Diagnostics.MappingMaterialization +Dapper.FluentMap.Diagnostics.MappingMaterialization.Dapper = 0 -> Dapper.FluentMap.Diagnostics.MappingMaterialization +Dapper.FluentMap.Diagnostics.MappingMaterialization.Nested = 1 -> Dapper.FluentMap.Diagnostics.MappingMaterialization +Dapper.FluentMap.Diagnostics.MappingMaterialization.ValueObject = 2 -> Dapper.FluentMap.Diagnostics.MappingMaterialization +Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.Diagnostics.MappingSource.Convention = 2 -> Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.Diagnostics.MappingSource.DapperDefault = 4 -> Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.Diagnostics.MappingSource.Explicit = 0 -> Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.Diagnostics.MappingSource.Inherited = 1 -> Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.Diagnostics.MappingSource.NamingPolicy = 3 -> Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.Diagnostics.MemberMappingExplanation +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.CaseSensitive.get -> bool +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.ColumnName.get -> string! +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.ConstructorParameters.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.ConventionType.get -> System.Type? +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Conversion.get -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Ignored.get -> bool +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.InheritedFrom.get -> System.Type? +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Materialization.get -> Dapper.FluentMap.Diagnostics.MappingMaterialization +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.MemberPath.get -> string! +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Persistence.get -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.PropertyInfo.get -> System.Reflection.PropertyInfo! +Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Source.get -> Dapper.FluentMap.Diagnostics.MappingSource +Dapper.FluentMap.FluentMapConfigurationException +Dapper.FluentMap.FluentMapConfigurationException.FluentMapConfigurationException() -> void +Dapper.FluentMap.FluentMapConfigurationException.FluentMapConfigurationException(string! message) -> void +Dapper.FluentMap.FluentMapConfigurationException.FluentMapConfigurationException(string! message, System.Exception! innerException) -> void +Dapper.FluentMap.FluentMapper +Dapper.FluentMap.FluentMapRuntime +Dapper.FluentMap.FluentMapRuntime.Configuration.get -> Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration! +Dapper.FluentMap.FluentMapRuntime.Explain() -> Dapper.FluentMap.Diagnostics.MappingExplanation! +Dapper.FluentMap.FluentMapRuntime.Explain() -> Dapper.FluentMap.Diagnostics.MappingExplanation! +Dapper.FluentMap.FluentMapRuntime.FluentMapRuntime(Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration! configuration) -> void +Dapper.FluentMap.FluentMapRuntime.QueryGeneratedMapped(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryGeneratedMapped(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryGeneratedMappedSingle(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! +Dapper.FluentMap.FluentMapRuntime.QueryGeneratedMappedSingle(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! +Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMappedSingle(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! +Dapper.FluentMap.FluentMapRuntime.QueryMappedSingle(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! +Dapper.FluentMap.FluentMapRuntime.QueryMappedUnbuffered(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMappedUnbuffered(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMappedUnbufferedAsync(System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMultipleMapped(System.Data.IDbConnection! connection, string! sql, object? param = null) -> Dapper.FluentMap.MappedGridReader! +Dapper.FluentMap.FluentMapRuntime.QueryMultipleMappedAsync(System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Dapper.FluentMap.FluentMapRuntime.Validate() -> void +Dapper.FluentMap.MappedGridReader +Dapper.FluentMap.MappedGridReader.Dispose() -> void +Dapper.FluentMap.MappedGridReader.DisposeAsync() -> System.Threading.Tasks.ValueTask +Dapper.FluentMap.MappedGridReader.IsConsumed.get -> bool +Dapper.FluentMap.MappedGridReader.ReadMapped() -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.MappedGridReader.ReadMapped() -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.MappedGridReader.ReadMappedAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Dapper.FluentMap.MappedGridReader.ReadMappedAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Dapper.FluentMap.MappedGridReader.ReadMappedSingle() -> TEntity! +Dapper.FluentMap.MappedGridReader.ReadMappedSingle() -> TEntity! +Dapper.FluentMap.MappedGridReader.ReadMappedSingleAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Dapper.FluentMap.MappedGridReader.ReadMappedSingleAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Dapper.FluentMap.Mapping.EntityMap +Dapper.FluentMap.Mapping.EntityMap.EntityMap() -> void +Dapper.FluentMap.Mapping.EntityMapBase +Dapper.FluentMap.Mapping.EntityMapBase.EntityMapBase() -> void +Dapper.FluentMap.Mapping.EntityMapBase.IncludeBase() -> void +Dapper.FluentMap.Mapping.EntityMapBase.Map(System.Linq.Expressions.Expression!>! expression) -> TPropertyMap +Dapper.FluentMap.Mapping.EntityMapBase.PropertyMaps.get -> System.Collections.Generic.IList! +Dapper.FluentMap.Mapping.IEntityMap +Dapper.FluentMap.Mapping.IEntityMap.PropertyMaps.get -> System.Collections.Generic.IList! +Dapper.FluentMap.Mapping.IEntityMap +Dapper.FluentMap.Mapping.IMappingProfile +Dapper.FluentMap.Mapping.IProfileMap +Dapper.FluentMap.Mapping.IPropertyConverter +Dapper.FluentMap.Mapping.IPropertyMap +Dapper.FluentMap.Mapping.IPropertyMap.CaseSensitive.get -> bool +Dapper.FluentMap.Mapping.IPropertyMap.ColumnName.get -> string! +Dapper.FluentMap.Mapping.IPropertyMap.Ignored.get -> bool +Dapper.FluentMap.Mapping.IPropertyMap.PropertyInfo.get -> System.Reflection.PropertyInfo! +Dapper.FluentMap.Mapping.IPropertyMapWithConversionMetadata +Dapper.FluentMap.Mapping.IPropertyMapWithConversionMetadata.Conversion.get -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! +Dapper.FluentMap.Mapping.IPropertyMapWithPersistenceMetadata +Dapper.FluentMap.Mapping.IPropertyMapWithPersistenceMetadata.Persistence.get -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! +Dapper.FluentMap.Mapping.IReadPropertyConverter +Dapper.FluentMap.Mapping.IReadPropertyConverter.ConvertFromDatabase(TDatabase value) -> TProperty +Dapper.FluentMap.Mapping.IWritePropertyConverter +Dapper.FluentMap.Mapping.IWritePropertyConverter.ConvertToDatabase(TProperty value) -> TDatabase +Dapper.FluentMap.Mapping.PropertyConversionDirection +Dapper.FluentMap.Mapping.PropertyConversionDirection.Read = 0 -> Dapper.FluentMap.Mapping.PropertyConversionDirection +Dapper.FluentMap.Mapping.PropertyConversionDirection.Write = 1 -> Dapper.FluentMap.Mapping.PropertyConversionDirection +Dapper.FluentMap.Mapping.PropertyConversionMetadata +Dapper.FluentMap.Mapping.PropertyConversionMetadata.HasReadConverter.get -> bool +Dapper.FluentMap.Mapping.PropertyConversionMetadata.HasWriteConverter.get -> bool +Dapper.FluentMap.Mapping.PropertyConversionMetadata.ReadConverter.get -> Dapper.FluentMap.Mapping.PropertyConverterMetadata? +Dapper.FluentMap.Mapping.PropertyConversionMetadata.WriteConverter.get -> Dapper.FluentMap.Mapping.PropertyConverterMetadata? +Dapper.FluentMap.Mapping.PropertyConverterMetadata +Dapper.FluentMap.Mapping.PropertyConverterMetadata.ConverterType.get -> System.Type! +Dapper.FluentMap.Mapping.PropertyConverterMetadata.DatabaseType.get -> System.Type! +Dapper.FluentMap.Mapping.PropertyConverterMetadata.Direction.get -> Dapper.FluentMap.Mapping.PropertyConversionDirection +Dapper.FluentMap.Mapping.PropertyConverterMetadata.PropertyType.get -> System.Type! +Dapper.FluentMap.Mapping.PropertyMap +Dapper.FluentMap.Mapping.PropertyMap.PropertyMap(System.Reflection.PropertyInfo! info) -> void +Dapper.FluentMap.Mapping.PropertyMap.PropertyMap(System.Reflection.PropertyInfo! info, string! columnName) -> void +Dapper.FluentMap.Mapping.PropertyMap.PropertyMap(System.Reflection.PropertyInfo! info, string! columnName, bool caseSensitive) -> void +Dapper.FluentMap.Mapping.PropertyMapBase +Dapper.FluentMap.Mapping.PropertyMapBase.CaseSensitive.get -> bool +Dapper.FluentMap.Mapping.PropertyMapBase.ColumnName.get -> string! +Dapper.FluentMap.Mapping.PropertyMapBase.Computed() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.Conversion.get -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertFromDatabaseUsing() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertFromDatabaseUsing(Dapper.FluentMap.Mapping.IReadPropertyConverter! converter) -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertFromDatabaseUsing(Dapper.FluentMap.Mapping.ReadPropertyConverter! converter) -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertToDatabaseUsing() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertToDatabaseUsing(Dapper.FluentMap.Mapping.IWritePropertyConverter! converter) -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertToDatabaseUsing(Dapper.FluentMap.Mapping.WritePropertyConverter! converter) -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertUsing() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ConvertUsing(Dapper.FluentMap.Mapping.IPropertyConverter! converter) -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.DatabaseDefaultOnInsert() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ExcludeFromInsert() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ExcludeFromUpdate() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.GetType() -> System.Type! +Dapper.FluentMap.Mapping.PropertyMapBase.Ignore() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.Ignored.get -> bool +Dapper.FluentMap.Mapping.PropertyMapBase.MarkAsComputed() -> void +Dapper.FluentMap.Mapping.PropertyMapBase.MarkAsIdentity() -> void +Dapper.FluentMap.Mapping.PropertyMapBase.MarkAsKey() -> void +Dapper.FluentMap.Mapping.PropertyMapBase.MarkAsNotGenerated() -> void +Dapper.FluentMap.Mapping.PropertyMapBase.Persistence.get -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! +Dapper.FluentMap.Mapping.PropertyMapBase.PropertyInfo.get -> System.Reflection.PropertyInfo! +Dapper.FluentMap.Mapping.PropertyMapBase.PropertyMapBase(System.Reflection.PropertyInfo! info) -> void +Dapper.FluentMap.Mapping.PropertyMapBase.ReadOnly() -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.ToColumn(string! columnName, bool caseSensitive = true) -> TPropertyMap! +Dapper.FluentMap.Mapping.PropertyMapBase.UsePersistence(Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! persistence) -> void +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.HasDatabaseDefaultOnInsert.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IgnoredByFluentMap.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IsComputed.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IsGenerated.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IsIdentity.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IsKey.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.ParticipatesInInsert.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.ParticipatesInMaterialization.get -> bool +Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.ParticipatesInUpdate.get -> bool +Dapper.FluentMap.Mapping.ReadPropertyConverter +Dapper.FluentMap.Mapping.WritePropertyConverter +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.ColumnName.get -> string! +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.Ignored.get -> bool +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.MemberPath.get -> string? +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.ReadConverterDatabaseType.get -> System.Type? +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.ReadConverterPropertyType.get -> System.Type? +Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.ReadConverterType.get -> System.Type? +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.Columns.get -> System.Collections.Generic.IReadOnlyList! +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.EntityType.get -> System.Type! +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.GeneratedMaterializerDescriptor(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> void +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.GeneratedMaterializerDescriptor(System.Type? profileType, System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> void +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.Materializer.get -> Dapper.FluentMap.Materialization.GeneratedRowMaterializer! +Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.ProfileType.get -> System.Type? +Dapper.FluentMap.Materialization.GeneratedRowMaterializer +Dapper.FluentMap.Naming.NamingPolicy +Dapper.FluentMap.Naming.NamingPolicy.GetColumnName(string! memberName) -> string! +Dapper.FluentMap.Naming.NamingPolicy.GetType() -> System.Type! +Dapper.FluentMap.Naming.NamingPolicy.Then(Dapper.FluentMap.Naming.NamingPolicy! next) -> Dapper.FluentMap.Naming.NamingPolicy! +Dapper.FluentMap.Naming.NamingPolicy.Then(System.Func! transformer) -> Dapper.FluentMap.Naming.NamingPolicy! +Dapper.FluentMap.Naming.NamingPolicy.WithPrefix(string! prefix) -> Dapper.FluentMap.Naming.NamingPolicy! +Dapper.FluentMap.Naming.NamingPolicy.WithSuffix(string! suffix) -> Dapper.FluentMap.Naming.NamingPolicy! +Dapper.FluentMap.QueryMappedExtensions +Dapper.FluentMap.TypeMaps.FluentConventionTypeMap +Dapper.FluentMap.TypeMaps.FluentConventionTypeMap.FluentConventionTypeMap() -> void +Dapper.FluentMap.TypeMaps.FluentMapTypeMap +Dapper.FluentMap.TypeMaps.FluentMapTypeMap.FluentMapTypeMap() -> void +Dapper.FluentMap.TypeMaps.MultiTypeMap +Dapper.FluentMap.TypeMaps.MultiTypeMap.FindConstructor(string![]! names, System.Type![]! types) -> System.Reflection.ConstructorInfo! +Dapper.FluentMap.TypeMaps.MultiTypeMap.FindExplicitConstructor() -> System.Reflection.ConstructorInfo! +Dapper.FluentMap.TypeMaps.MultiTypeMap.GetConstructorParameter(System.Reflection.ConstructorInfo! constructor, string! columnName) -> Dapper.SqlMapper.IMemberMap! +Dapper.FluentMap.TypeMaps.MultiTypeMap.GetMember(string! columnName) -> Dapper.SqlMapper.IMemberMap! +Dapper.FluentMap.TypeMaps.MultiTypeMap.MultiTypeMap(params Dapper.SqlMapper.ITypeMap![]! mappers) -> void +Dapper.FluentMap.Utils.FluentMapConfigurationExtensions +Dapper.FluentMap.Utils.ReflectionHelper +override Dapper.FluentMap.Configuration.FluentConventionConfiguration.Equals(object! obj) -> bool +override Dapper.FluentMap.Configuration.FluentConventionConfiguration.GetHashCode() -> int +override Dapper.FluentMap.Configuration.FluentConventionConfiguration.ToString() -> string! +override Dapper.FluentMap.Configuration.FluentMapConfiguration.Equals(object! obj) -> bool +override Dapper.FluentMap.Configuration.FluentMapConfiguration.GetHashCode() -> int +override Dapper.FluentMap.Configuration.FluentMapConfiguration.ToString() -> string! +override Dapper.FluentMap.Conventions.Convention.Equals(object! obj) -> bool +override Dapper.FluentMap.Conventions.Convention.GetHashCode() -> int +override Dapper.FluentMap.Conventions.Convention.ToString() -> string! +override Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.Equals(object! obj) -> bool +override Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.GetHashCode() -> int +override Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.ToString() -> string! +override Dapper.FluentMap.Conventions.PropertyConventionConfiguration.Equals(object! obj) -> bool +override Dapper.FluentMap.Conventions.PropertyConventionConfiguration.GetHashCode() -> int +override Dapper.FluentMap.Conventions.PropertyConventionConfiguration.ToString() -> string! +override Dapper.FluentMap.Diagnostics.MappingExplanation.ToString() -> string! +override Dapper.FluentMap.Mapping.EntityMap.GetPropertyMap(System.Reflection.PropertyInfo! info) -> Dapper.FluentMap.Mapping.PropertyMap! +override Dapper.FluentMap.Mapping.PropertyMapBase.Equals(object! obj) -> bool +override Dapper.FluentMap.Mapping.PropertyMapBase.GetHashCode() -> int +override Dapper.FluentMap.Mapping.PropertyMapBase.ToString() -> string! +override Dapper.FluentMap.Naming.NamingPolicy.Equals(object! obj) -> bool +override Dapper.FluentMap.Naming.NamingPolicy.GetHashCode() -> int +override Dapper.FluentMap.Naming.NamingPolicy.ToString() -> string! +static Dapper.FluentMap.FluentMapper.Configuration.get -> Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration! +static Dapper.FluentMap.FluentMapper.Explain() -> Dapper.FluentMap.Diagnostics.MappingExplanation! +static Dapper.FluentMap.FluentMapper.Explain() -> Dapper.FluentMap.Diagnostics.MappingExplanation! +static Dapper.FluentMap.FluentMapper.GetEntityMaps() -> System.Collections.Generic.IReadOnlyDictionary! +static Dapper.FluentMap.FluentMapper.GetTypeConventions() -> System.Collections.Generic.IReadOnlyDictionary!>! +static Dapper.FluentMap.FluentMapper.Initialize(System.Action! configure) -> void +static Dapper.FluentMap.FluentMapper.Runtime.get -> Dapper.FluentMap.FluentMapRuntime! +static Dapper.FluentMap.FluentMapper.Validate() -> void +static Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.Ignore(string! columnName) -> Dapper.FluentMap.Materialization.GeneratedMaterializerColumn! +static Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.Map(string! columnName, string! memberPath) -> Dapper.FluentMap.Materialization.GeneratedMaterializerColumn! +static Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.Map(string! columnName, string! memberPath, System.Type! readConverterType, System.Type! readConverterDatabaseType, System.Type! readConverterPropertyType) -> Dapper.FluentMap.Materialization.GeneratedMaterializerColumn! +static Dapper.FluentMap.Naming.NamingPolicy.Custom(System.Func! transformer) -> Dapper.FluentMap.Naming.NamingPolicy! +static Dapper.FluentMap.Naming.NamingPolicy.Identity.get -> Dapper.FluentMap.Naming.NamingPolicy! +static Dapper.FluentMap.Naming.NamingPolicy.Prefix(string! prefix) -> Dapper.FluentMap.Naming.NamingPolicy! +static Dapper.FluentMap.Naming.NamingPolicy.SnakeCase.get -> Dapper.FluentMap.Naming.NamingPolicy! +static Dapper.FluentMap.Naming.NamingPolicy.Suffix(string! suffix) -> Dapper.FluentMap.Naming.NamingPolicy! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task!>! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedSingle(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedSingle(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedSingleAsync(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbuffered(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbuffered(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbuffered(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbuffered(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, string! sql, System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, string! sql, System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMultipleMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> Dapper.FluentMap.MappedGridReader! +static Dapper.FluentMap.QueryMappedExtensions.QueryMultipleMapped(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> Dapper.FluentMap.MappedGridReader! +static Dapper.FluentMap.QueryMappedExtensions.QueryMultipleMappedAsync(this System.Data.Common.DbConnection! connection, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.FluentMap.QueryMappedExtensions.QueryMultipleMappedAsync(this System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static Dapper.FluentMap.TypeMaps.MultiTypeMap.MatchColumnNames(Dapper.FluentMap.Mapping.IPropertyMap! map, string! columnName) -> bool +static Dapper.FluentMap.TypeMaps.MultiTypeMap.TypePropertyMapCache.get -> System.Collections.Concurrent.ConcurrentDictionary! +static Dapper.FluentMap.Utils.FluentMapConfigurationExtensions.ApplyMapsFromAssemblies(this Dapper.FluentMap.Configuration.FluentMapConfiguration! configuration, params System.Reflection.Assembly![]! assemblies) -> void +static Dapper.FluentMap.Utils.ReflectionHelper.GetMemberInfo(System.Linq.Expressions.LambdaExpression! lambda) -> System.Reflection.MemberInfo! +static readonly Dapper.FluentMap.FluentMapper.EntityMaps -> System.Collections.Concurrent.ConcurrentDictionary! +static readonly Dapper.FluentMap.FluentMapper.TypeConventions -> System.Collections.Concurrent.ConcurrentDictionary!>! +static readonly Dapper.FluentMap.Mapping.PropertyConversionMetadata.Default -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! +static readonly Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.Default -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! +static readonly Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.Ignored -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! +virtual Dapper.FluentMap.Mapping.ReadPropertyConverter.Invoke(TDatabase value) -> TProperty +virtual Dapper.FluentMap.Mapping.WritePropertyConverter.Invoke(TProperty value) -> TDatabase +virtual Dapper.FluentMap.Materialization.GeneratedRowMaterializer.Invoke(System.Data.IDataRecord! record) -> TEntity! diff --git a/src/Dapper.FluentMap/PublicAPI.Unshipped.txt b/src/Dapper.FluentMap/PublicAPI.Unshipped.txt index 73112db..011a2f7 100644 --- a/src/Dapper.FluentMap/PublicAPI.Unshipped.txt +++ b/src/Dapper.FluentMap/PublicAPI.Unshipped.txt @@ -1,380 +1,20 @@ #nullable enable -abstract Dapper.FluentMap.Mapping.EntityMapBase.GetPropertyMap(System.Reflection.PropertyInfo! info) -> TPropertyMap -Dapper.FluentMap.Configuration.ConventionMappingConfiguration -Dapper.FluentMap.Configuration.ConventionMappingConfiguration.ConventionType.get -> System.Type! -Dapper.FluentMap.Configuration.ConventionMappingConfiguration.EntityType.get -> System.Type! -Dapper.FluentMap.Configuration.ConventionMappingConfiguration.PropertyMaps.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Configuration.EntityMappingConfiguration -Dapper.FluentMap.Configuration.EntityMappingConfiguration.EntityType.get -> System.Type! -Dapper.FluentMap.Configuration.EntityMappingConfiguration.IncludedBaseTypes.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Configuration.EntityMappingConfiguration.MapType.get -> System.Type! -Dapper.FluentMap.Configuration.EntityMappingConfiguration.PropertyMaps.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Configuration.FluentConventionConfiguration -Dapper.FluentMap.Configuration.FluentConventionConfiguration.FluentConventionConfiguration(Dapper.FluentMap.Conventions.Convention! convention) -> void -Dapper.FluentMap.Configuration.FluentConventionConfiguration.ForEntitiesInAssembly(System.Reflection.Assembly! assembly, params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! -Dapper.FluentMap.Configuration.FluentConventionConfiguration.ForEntitiesInCurrentAssembly(params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! -Dapper.FluentMap.Configuration.FluentConventionConfiguration.ForEntity() -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! -Dapper.FluentMap.Configuration.FluentConventionConfiguration.GetType() -> System.Type! -Dapper.FluentMap.Configuration.FluentMapConfiguration -Dapper.FluentMap.Configuration.FluentMapConfiguration.AddConvention() -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfiguration.AddGeneratedMaterializer(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfiguration.AddGeneratedMaterializer(Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor! descriptor) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfiguration.AddGeneratedMaterializer(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfiguration.AddMap(Dapper.FluentMap.Mapping.IEntityMap! mapper) -> void -Dapper.FluentMap.Configuration.FluentMapConfiguration.AddMap() -> Dapper.FluentMap.Configuration.FluentMapConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfiguration.AddMapsFromAssembly(System.Reflection.Assembly! assembly, params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfiguration.AddMapsFromAssemblyContaining(params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentMapConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfiguration.AddProfile() -> Dapper.FluentMap.Configuration.FluentMapConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfiguration.FluentMapConfiguration() -> void -Dapper.FluentMap.Configuration.FluentMapConfiguration.GetType() -> System.Type! -Dapper.FluentMap.Configuration.FluentMapConfiguration.UseNamingPolicy(Dapper.FluentMap.Naming.NamingPolicy! namingPolicy, bool caseSensitive = true) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfiguration.UseNamingPolicy(System.Func! transformer, bool caseSensitive = true) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddConvention() -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddGeneratedMaterializer(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddGeneratedMaterializer(Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor! descriptor) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddGeneratedMaterializer(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddMap(Dapper.FluentMap.Mapping.IEntityMap! mapper) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddMap() -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddMapsFromAssembly(System.Reflection.Assembly! assembly, params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddMapsFromAssemblyContaining(params string![]! namespaces) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.AddProfile() -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.Build() -> Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.Configure(System.Action! configure) -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.FluentMapConfigurationBuilder() -> void -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.UseNamingPolicy(Dapper.FluentMap.Naming.NamingPolicy! namingPolicy, bool caseSensitive = true) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.UseNamingPolicy(System.Func! transformer, bool caseSensitive = true) -> Dapper.FluentMap.Configuration.FluentConventionConfiguration! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.UseStrictGeneratedMaterialization() -> Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder! -Dapper.FluentMap.Configuration.FluentMapConfigurationBuilder.Validate() -> void -Dapper.FluentMap.Configuration.GeneratedMaterializerConfiguration -Dapper.FluentMap.Configuration.GeneratedMaterializerConfiguration.Columns.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Configuration.GeneratedMaterializerConfiguration.EntityType.get -> System.Type! -Dapper.FluentMap.Configuration.GeneratedMaterializerConfiguration.ProfileType.get -> System.Type! -Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration -Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.CreateRuntime() -> Dapper.FluentMap.FluentMapRuntime! -Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.EntityMaps.get -> System.Collections.Generic.IReadOnlyDictionary! -Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.GeneratedMaterializers.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.ProfileMaps.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.StrictGeneratedMaterialization.get -> bool -Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration.TypeConventions.get -> System.Collections.Generic.IReadOnlyDictionary!>! -Dapper.FluentMap.Configuration.ProfileMappingConfiguration -Dapper.FluentMap.Configuration.ProfileMappingConfiguration.EntityType.get -> System.Type! -Dapper.FluentMap.Configuration.ProfileMappingConfiguration.IncludedBaseTypes.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Configuration.ProfileMappingConfiguration.MapType.get -> System.Type! -Dapper.FluentMap.Configuration.ProfileMappingConfiguration.ProfileType.get -> System.Type? -Dapper.FluentMap.Configuration.ProfileMappingConfiguration.PropertyMaps.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Configuration.PropertyMappingConfiguration -Dapper.FluentMap.Configuration.PropertyMappingConfiguration.CaseSensitive.get -> bool -Dapper.FluentMap.Configuration.PropertyMappingConfiguration.ColumnName.get -> string! -Dapper.FluentMap.Configuration.PropertyMappingConfiguration.Conversion.get -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! -Dapper.FluentMap.Configuration.PropertyMappingConfiguration.Ignored.get -> bool -Dapper.FluentMap.Configuration.PropertyMappingConfiguration.MemberPath.get -> string! -Dapper.FluentMap.Configuration.PropertyMappingConfiguration.Persistence.get -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! -Dapper.FluentMap.Configuration.PropertyMappingConfiguration.PropertyInfo.get -> System.Reflection.PropertyInfo! -Dapper.FluentMap.Conventions.Convention -Dapper.FluentMap.Conventions.Convention.Convention() -> void -Dapper.FluentMap.Conventions.Convention.ConventionConfigurations.get -> System.Collections.Generic.IList! -Dapper.FluentMap.Conventions.Convention.GetType() -> System.Type! -Dapper.FluentMap.Conventions.Convention.Properties() -> Dapper.FluentMap.Conventions.PropertyConventionConfiguration! -Dapper.FluentMap.Conventions.Convention.Properties() -> Dapper.FluentMap.Conventions.PropertyConventionConfiguration! -Dapper.FluentMap.Conventions.Convention.PropertyMaps.get -> System.Collections.Generic.IList! -Dapper.FluentMap.Conventions.ConventionPropertyConfiguration -Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.ConventionPropertyConfiguration() -> void -Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.GetType() -> System.Type! -Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.HasColumnName(string! columnName) -> Dapper.FluentMap.Conventions.ConventionPropertyConfiguration! -Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.HasPrefix(string! prefix) -> Dapper.FluentMap.Conventions.ConventionPropertyConfiguration! -Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.IsCaseInsensitive() -> Dapper.FluentMap.Conventions.ConventionPropertyConfiguration! -Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.Transform(System.Func! transformer) -> Dapper.FluentMap.Conventions.ConventionPropertyConfiguration! -Dapper.FluentMap.Conventions.PropertyConventionConfiguration -Dapper.FluentMap.Conventions.PropertyConventionConfiguration.Configure(System.Action! configure) -> void -Dapper.FluentMap.Conventions.PropertyConventionConfiguration.GetType() -> System.Type! -Dapper.FluentMap.Conventions.PropertyConventionConfiguration.PropertyConventionConfiguration() -> void -Dapper.FluentMap.Conventions.PropertyConventionConfiguration.Where(System.Func! predicate) -> Dapper.FluentMap.Conventions.PropertyConventionConfiguration! -Dapper.FluentMap.Diagnostics.ConstructorParameterExplanation -Dapper.FluentMap.Diagnostics.ConstructorParameterExplanation.Constructor.get -> System.Reflection.ConstructorInfo! -Dapper.FluentMap.Diagnostics.ConstructorParameterExplanation.Name.get -> string! -Dapper.FluentMap.Diagnostics.ConstructorParameterExplanation.ParameterType.get -> System.Type! -Dapper.FluentMap.Diagnostics.MappingExplanation -Dapper.FluentMap.Diagnostics.MappingExplanation.ConventionTypes.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Diagnostics.MappingExplanation.Diagnostics.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Diagnostics.MappingExplanation.EntityMapType.get -> System.Type? -Dapper.FluentMap.Diagnostics.MappingExplanation.EntityType.get -> System.Type! -Dapper.FluentMap.Diagnostics.MappingExplanation.Members.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Diagnostics.MappingExplanation.ProfileType.get -> System.Type? -Dapper.FluentMap.Diagnostics.MappingMaterialization -Dapper.FluentMap.Diagnostics.MappingMaterialization.Dapper = 0 -> Dapper.FluentMap.Diagnostics.MappingMaterialization -Dapper.FluentMap.Diagnostics.MappingMaterialization.Nested = 1 -> Dapper.FluentMap.Diagnostics.MappingMaterialization -Dapper.FluentMap.Diagnostics.MappingMaterialization.ValueObject = 2 -> Dapper.FluentMap.Diagnostics.MappingMaterialization -Dapper.FluentMap.Diagnostics.MappingSource -Dapper.FluentMap.Diagnostics.MappingSource.Convention = 2 -> Dapper.FluentMap.Diagnostics.MappingSource -Dapper.FluentMap.Diagnostics.MappingSource.DapperDefault = 4 -> Dapper.FluentMap.Diagnostics.MappingSource -Dapper.FluentMap.Diagnostics.MappingSource.Explicit = 0 -> Dapper.FluentMap.Diagnostics.MappingSource -Dapper.FluentMap.Diagnostics.MappingSource.Inherited = 1 -> Dapper.FluentMap.Diagnostics.MappingSource -Dapper.FluentMap.Diagnostics.MappingSource.NamingPolicy = 3 -> Dapper.FluentMap.Diagnostics.MappingSource -Dapper.FluentMap.Diagnostics.MemberMappingExplanation -Dapper.FluentMap.Diagnostics.MemberMappingExplanation.CaseSensitive.get -> bool -Dapper.FluentMap.Diagnostics.MemberMappingExplanation.ColumnName.get -> string! -Dapper.FluentMap.Diagnostics.MemberMappingExplanation.ConstructorParameters.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Diagnostics.MemberMappingExplanation.ConventionType.get -> System.Type? -Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Conversion.get -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! -Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Ignored.get -> bool -Dapper.FluentMap.Diagnostics.MemberMappingExplanation.InheritedFrom.get -> System.Type? -Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Materialization.get -> Dapper.FluentMap.Diagnostics.MappingMaterialization -Dapper.FluentMap.Diagnostics.MemberMappingExplanation.MemberPath.get -> string! -Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Persistence.get -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! -Dapper.FluentMap.Diagnostics.MemberMappingExplanation.PropertyInfo.get -> System.Reflection.PropertyInfo! -Dapper.FluentMap.Diagnostics.MemberMappingExplanation.Source.get -> Dapper.FluentMap.Diagnostics.MappingSource -Dapper.FluentMap.FluentMapConfigurationException -Dapper.FluentMap.FluentMapConfigurationException.FluentMapConfigurationException() -> void -Dapper.FluentMap.FluentMapConfigurationException.FluentMapConfigurationException(string! message) -> void -Dapper.FluentMap.FluentMapConfigurationException.FluentMapConfigurationException(string! message, System.Exception! innerException) -> void -Dapper.FluentMap.FluentMapper -Dapper.FluentMap.FluentMapRuntime -Dapper.FluentMap.FluentMapRuntime.Configuration.get -> Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration! -Dapper.FluentMap.FluentMapRuntime.Explain() -> Dapper.FluentMap.Diagnostics.MappingExplanation! -Dapper.FluentMap.FluentMapRuntime.Explain() -> Dapper.FluentMap.Diagnostics.MappingExplanation! -Dapper.FluentMap.FluentMapRuntime.FluentMapRuntime(Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration! configuration) -> void -Dapper.FluentMap.FluentMapRuntime.QueryGeneratedMapped(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! -Dapper.FluentMap.FluentMapRuntime.QueryGeneratedMapped(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! -Dapper.FluentMap.FluentMapRuntime.QueryGeneratedMappedSingle(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! -Dapper.FluentMap.FluentMapRuntime.QueryGeneratedMappedSingle(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! -Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! -Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! -Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! -Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! -Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null) -> System.Collections.Generic.IEnumerable! -Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null) -> System.Collections.Generic.IEnumerable! -Dapper.FluentMap.FluentMapRuntime.QueryMappedAsync(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! -Dapper.FluentMap.FluentMapRuntime.QueryMappedAsync(System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! -Dapper.FluentMap.FluentMapRuntime.QueryMappedSingle(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! -Dapper.FluentMap.FluentMapRuntime.QueryMappedSingle(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! -Dapper.FluentMap.FluentMapRuntime.QueryMappedUnbuffered(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! -Dapper.FluentMap.FluentMapRuntime.QueryMappedUnbuffered(System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! -Dapper.FluentMap.FluentMapRuntime.QueryMappedUnbufferedAsync(System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! -Dapper.FluentMap.FluentMapRuntime.QueryMultipleMapped(System.Data.IDbConnection! connection, string! sql, object? param = null) -> Dapper.FluentMap.MappedGridReader! -Dapper.FluentMap.FluentMapRuntime.QueryMultipleMappedAsync(System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Dapper.FluentMap.FluentMapRuntime.Validate() -> void +Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, System.Func! map, string! splitOn, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMapped(System.Data.IDbConnection! connection, string! sql, System.Func! map, string! splitOn, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +Dapper.FluentMap.FluentMapRuntime.QueryMappedAsync(System.Data.IDbConnection! connection, string! sql, System.Func! map, string! splitOn, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Dapper.FluentMap.FluentMapRuntime.QueryMappedAsync(System.Data.IDbConnection! connection, string! sql, System.Func! map, string! splitOn, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! Dapper.FluentMap.GeneratedParameters Dapper.FluentMap.GeneratedParameters.Add(string! name, object? value, System.Data.DbType dbType, int? size = null) -> Dapper.FluentMap.GeneratedParameters! Dapper.FluentMap.GeneratedParameters.GeneratedParameters() -> void -Dapper.FluentMap.MappedGridReader -Dapper.FluentMap.MappedGridReader.Dispose() -> void -Dapper.FluentMap.MappedGridReader.DisposeAsync() -> System.Threading.Tasks.ValueTask -Dapper.FluentMap.MappedGridReader.IsConsumed.get -> bool -Dapper.FluentMap.MappedGridReader.ReadMapped() -> System.Collections.Generic.IEnumerable! -Dapper.FluentMap.MappedGridReader.ReadMapped() -> System.Collections.Generic.IEnumerable! -Dapper.FluentMap.MappedGridReader.ReadMappedAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! -Dapper.FluentMap.MappedGridReader.ReadMappedAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! -Dapper.FluentMap.MappedGridReader.ReadMappedSingle() -> TEntity! -Dapper.FluentMap.MappedGridReader.ReadMappedSingle() -> TEntity! -Dapper.FluentMap.MappedGridReader.ReadMappedSingleAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Dapper.FluentMap.MappedGridReader.ReadMappedSingleAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Dapper.FluentMap.Mapping.EntityMap -Dapper.FluentMap.Mapping.EntityMap.EntityMap() -> void -Dapper.FluentMap.Mapping.EntityMapBase Dapper.FluentMap.Mapping.EntityMapBase.ConstructUsing(System.Linq.Expressions.Expression!>! value1, System.Linq.Expressions.Expression!>! value2, System.Linq.Expressions.Expression!>! value3, System.Linq.Expressions.Expression!>! value4, System.Func! factory) -> void Dapper.FluentMap.Mapping.EntityMapBase.ConstructUsing(System.Linq.Expressions.Expression!>! value1, System.Linq.Expressions.Expression!>! value2, System.Linq.Expressions.Expression!>! value3, System.Func! factory) -> void Dapper.FluentMap.Mapping.EntityMapBase.ConstructUsing(System.Linq.Expressions.Expression!>! value1, System.Linq.Expressions.Expression!>! value2, System.Func! factory) -> void Dapper.FluentMap.Mapping.EntityMapBase.ConstructUsing(System.Linq.Expressions.Expression!>! value1, System.Func! factory) -> void -Dapper.FluentMap.Mapping.EntityMapBase.EntityMapBase() -> void -Dapper.FluentMap.Mapping.EntityMapBase.IncludeBase() -> void -Dapper.FluentMap.Mapping.EntityMapBase.Map(System.Linq.Expressions.Expression!>! expression) -> TPropertyMap -Dapper.FluentMap.Mapping.EntityMapBase.PropertyMaps.get -> System.Collections.Generic.IList! -Dapper.FluentMap.Mapping.IEntityMap -Dapper.FluentMap.Mapping.IEntityMap.PropertyMaps.get -> System.Collections.Generic.IList! -Dapper.FluentMap.Mapping.IEntityMap -Dapper.FluentMap.Mapping.IMappingProfile -Dapper.FluentMap.Mapping.IProfileMap -Dapper.FluentMap.Mapping.IPropertyConverter -Dapper.FluentMap.Mapping.IPropertyMap -Dapper.FluentMap.Mapping.IPropertyMap.CaseSensitive.get -> bool -Dapper.FluentMap.Mapping.IPropertyMap.ColumnName.get -> string! -Dapper.FluentMap.Mapping.IPropertyMap.Ignored.get -> bool -Dapper.FluentMap.Mapping.IPropertyMap.PropertyInfo.get -> System.Reflection.PropertyInfo! -Dapper.FluentMap.Mapping.IPropertyMapWithConversionMetadata -Dapper.FluentMap.Mapping.IPropertyMapWithConversionMetadata.Conversion.get -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! -Dapper.FluentMap.Mapping.IPropertyMapWithPersistenceMetadata -Dapper.FluentMap.Mapping.IPropertyMapWithPersistenceMetadata.Persistence.get -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! -Dapper.FluentMap.Mapping.IReadPropertyConverter -Dapper.FluentMap.Mapping.IReadPropertyConverter.ConvertFromDatabase(TDatabase value) -> TProperty -Dapper.FluentMap.Mapping.IWritePropertyConverter -Dapper.FluentMap.Mapping.IWritePropertyConverter.ConvertToDatabase(TProperty value) -> TDatabase -Dapper.FluentMap.Mapping.PropertyConversionDirection -Dapper.FluentMap.Mapping.PropertyConversionDirection.Read = 0 -> Dapper.FluentMap.Mapping.PropertyConversionDirection -Dapper.FluentMap.Mapping.PropertyConversionDirection.Write = 1 -> Dapper.FluentMap.Mapping.PropertyConversionDirection -Dapper.FluentMap.Mapping.PropertyConversionMetadata -Dapper.FluentMap.Mapping.PropertyConversionMetadata.HasReadConverter.get -> bool -Dapper.FluentMap.Mapping.PropertyConversionMetadata.HasWriteConverter.get -> bool -Dapper.FluentMap.Mapping.PropertyConversionMetadata.ReadConverter.get -> Dapper.FluentMap.Mapping.PropertyConverterMetadata? -Dapper.FluentMap.Mapping.PropertyConversionMetadata.WriteConverter.get -> Dapper.FluentMap.Mapping.PropertyConverterMetadata? -Dapper.FluentMap.Mapping.PropertyConverterMetadata -Dapper.FluentMap.Mapping.PropertyConverterMetadata.ConverterType.get -> System.Type! -Dapper.FluentMap.Mapping.PropertyConverterMetadata.DatabaseType.get -> System.Type! -Dapper.FluentMap.Mapping.PropertyConverterMetadata.Direction.get -> Dapper.FluentMap.Mapping.PropertyConversionDirection -Dapper.FluentMap.Mapping.PropertyConverterMetadata.PropertyType.get -> System.Type! -Dapper.FluentMap.Mapping.PropertyMap -Dapper.FluentMap.Mapping.PropertyMap.PropertyMap(System.Reflection.PropertyInfo! info) -> void -Dapper.FluentMap.Mapping.PropertyMap.PropertyMap(System.Reflection.PropertyInfo! info, string! columnName) -> void -Dapper.FluentMap.Mapping.PropertyMap.PropertyMap(System.Reflection.PropertyInfo! info, string! columnName, bool caseSensitive) -> void -Dapper.FluentMap.Mapping.PropertyMapBase -Dapper.FluentMap.Mapping.PropertyMapBase.CaseSensitive.get -> bool -Dapper.FluentMap.Mapping.PropertyMapBase.ColumnName.get -> string! -Dapper.FluentMap.Mapping.PropertyMapBase.Computed() -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.Conversion.get -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! -Dapper.FluentMap.Mapping.PropertyMapBase.ConvertFromDatabaseUsing() -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.ConvertFromDatabaseUsing(Dapper.FluentMap.Mapping.IReadPropertyConverter! converter) -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.ConvertFromDatabaseUsing(Dapper.FluentMap.Mapping.ReadPropertyConverter! converter) -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.ConvertToDatabaseUsing() -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.ConvertToDatabaseUsing(Dapper.FluentMap.Mapping.IWritePropertyConverter! converter) -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.ConvertToDatabaseUsing(Dapper.FluentMap.Mapping.WritePropertyConverter! converter) -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.ConvertUsing() -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.ConvertUsing(Dapper.FluentMap.Mapping.IPropertyConverter! converter) -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.DatabaseDefaultOnInsert() -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.ExcludeFromInsert() -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.ExcludeFromUpdate() -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.GetType() -> System.Type! -Dapper.FluentMap.Mapping.PropertyMapBase.Ignore() -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.Ignored.get -> bool -Dapper.FluentMap.Mapping.PropertyMapBase.MarkAsComputed() -> void -Dapper.FluentMap.Mapping.PropertyMapBase.MarkAsIdentity() -> void -Dapper.FluentMap.Mapping.PropertyMapBase.MarkAsKey() -> void -Dapper.FluentMap.Mapping.PropertyMapBase.MarkAsNotGenerated() -> void -Dapper.FluentMap.Mapping.PropertyMapBase.Persistence.get -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! -Dapper.FluentMap.Mapping.PropertyMapBase.PropertyInfo.get -> System.Reflection.PropertyInfo! -Dapper.FluentMap.Mapping.PropertyMapBase.PropertyMapBase(System.Reflection.PropertyInfo! info) -> void -Dapper.FluentMap.Mapping.PropertyMapBase.ReadOnly() -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.ToColumn(string! columnName, bool caseSensitive = true) -> TPropertyMap! -Dapper.FluentMap.Mapping.PropertyMapBase.UsePersistence(Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! persistence) -> void -Dapper.FluentMap.Mapping.PropertyPersistenceMetadata -Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.HasDatabaseDefaultOnInsert.get -> bool -Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IgnoredByFluentMap.get -> bool -Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IsComputed.get -> bool -Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IsGenerated.get -> bool -Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IsIdentity.get -> bool -Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.IsKey.get -> bool -Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.ParticipatesInInsert.get -> bool -Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.ParticipatesInMaterialization.get -> bool -Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.ParticipatesInUpdate.get -> bool -Dapper.FluentMap.Mapping.ReadPropertyConverter -Dapper.FluentMap.Mapping.WritePropertyConverter -Dapper.FluentMap.Materialization.GeneratedMaterializerColumn -Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.ColumnName.get -> string! -Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.Ignored.get -> bool -Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.MemberPath.get -> string? -Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.ReadConverterDatabaseType.get -> System.Type? -Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.ReadConverterPropertyType.get -> System.Type? -Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.ReadConverterType.get -> System.Type? -Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor -Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.Columns.get -> System.Collections.Generic.IReadOnlyList! -Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.EntityType.get -> System.Type! -Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.GeneratedMaterializerDescriptor(System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> void -Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.GeneratedMaterializerDescriptor(System.Type? profileType, System.Collections.Generic.IEnumerable! columns, Dapper.FluentMap.Materialization.GeneratedRowMaterializer! materializer) -> void -Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.Materializer.get -> Dapper.FluentMap.Materialization.GeneratedRowMaterializer! -Dapper.FluentMap.Materialization.GeneratedMaterializerDescriptor.ProfileType.get -> System.Type? -Dapper.FluentMap.Materialization.GeneratedRowMaterializer -Dapper.FluentMap.Naming.NamingPolicy -Dapper.FluentMap.Naming.NamingPolicy.GetColumnName(string! memberName) -> string! -Dapper.FluentMap.Naming.NamingPolicy.GetType() -> System.Type! -Dapper.FluentMap.Naming.NamingPolicy.Then(Dapper.FluentMap.Naming.NamingPolicy! next) -> Dapper.FluentMap.Naming.NamingPolicy! -Dapper.FluentMap.Naming.NamingPolicy.Then(System.Func! transformer) -> Dapper.FluentMap.Naming.NamingPolicy! -Dapper.FluentMap.Naming.NamingPolicy.WithPrefix(string! prefix) -> Dapper.FluentMap.Naming.NamingPolicy! -Dapper.FluentMap.Naming.NamingPolicy.WithSuffix(string! suffix) -> Dapper.FluentMap.Naming.NamingPolicy! -Dapper.FluentMap.QueryMappedExtensions -Dapper.FluentMap.TypeMaps.FluentConventionTypeMap -Dapper.FluentMap.TypeMaps.FluentConventionTypeMap.FluentConventionTypeMap() -> void -Dapper.FluentMap.TypeMaps.FluentMapTypeMap -Dapper.FluentMap.TypeMaps.FluentMapTypeMap.FluentMapTypeMap() -> void -Dapper.FluentMap.TypeMaps.MultiTypeMap -Dapper.FluentMap.TypeMaps.MultiTypeMap.FindConstructor(string![]! names, System.Type![]! types) -> System.Reflection.ConstructorInfo! -Dapper.FluentMap.TypeMaps.MultiTypeMap.FindExplicitConstructor() -> System.Reflection.ConstructorInfo! -Dapper.FluentMap.TypeMaps.MultiTypeMap.GetConstructorParameter(System.Reflection.ConstructorInfo! constructor, string! columnName) -> Dapper.SqlMapper.IMemberMap! -Dapper.FluentMap.TypeMaps.MultiTypeMap.GetMember(string! columnName) -> Dapper.SqlMapper.IMemberMap! -Dapper.FluentMap.TypeMaps.MultiTypeMap.MultiTypeMap(params Dapper.SqlMapper.ITypeMap![]! mappers) -> void -Dapper.FluentMap.Utils.FluentMapConfigurationExtensions -Dapper.FluentMap.Utils.ReflectionHelper -override Dapper.FluentMap.Configuration.FluentConventionConfiguration.Equals(object! obj) -> bool -override Dapper.FluentMap.Configuration.FluentConventionConfiguration.GetHashCode() -> int -override Dapper.FluentMap.Configuration.FluentConventionConfiguration.ToString() -> string! -override Dapper.FluentMap.Configuration.FluentMapConfiguration.Equals(object! obj) -> bool -override Dapper.FluentMap.Configuration.FluentMapConfiguration.GetHashCode() -> int -override Dapper.FluentMap.Configuration.FluentMapConfiguration.ToString() -> string! -override Dapper.FluentMap.Conventions.Convention.Equals(object! obj) -> bool -override Dapper.FluentMap.Conventions.Convention.GetHashCode() -> int -override Dapper.FluentMap.Conventions.Convention.ToString() -> string! -override Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.Equals(object! obj) -> bool -override Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.GetHashCode() -> int -override Dapper.FluentMap.Conventions.ConventionPropertyConfiguration.ToString() -> string! -override Dapper.FluentMap.Conventions.PropertyConventionConfiguration.Equals(object! obj) -> bool -override Dapper.FluentMap.Conventions.PropertyConventionConfiguration.GetHashCode() -> int -override Dapper.FluentMap.Conventions.PropertyConventionConfiguration.ToString() -> string! -override Dapper.FluentMap.Diagnostics.MappingExplanation.ToString() -> string! -override Dapper.FluentMap.Mapping.EntityMap.GetPropertyMap(System.Reflection.PropertyInfo! info) -> Dapper.FluentMap.Mapping.PropertyMap! -override Dapper.FluentMap.Mapping.PropertyMapBase.Equals(object! obj) -> bool -override Dapper.FluentMap.Mapping.PropertyMapBase.GetHashCode() -> int -override Dapper.FluentMap.Mapping.PropertyMapBase.ToString() -> string! -override Dapper.FluentMap.Naming.NamingPolicy.Equals(object! obj) -> bool -override Dapper.FluentMap.Naming.NamingPolicy.GetHashCode() -> int -override Dapper.FluentMap.Naming.NamingPolicy.ToString() -> string! -static Dapper.FluentMap.FluentMapper.Configuration.get -> Dapper.FluentMap.Configuration.ImmutableFluentMapConfiguration! -static Dapper.FluentMap.FluentMapper.Explain() -> Dapper.FluentMap.Diagnostics.MappingExplanation! -static Dapper.FluentMap.FluentMapper.Explain() -> Dapper.FluentMap.Diagnostics.MappingExplanation! -static Dapper.FluentMap.FluentMapper.GetEntityMaps() -> System.Collections.Generic.IReadOnlyDictionary! -static Dapper.FluentMap.FluentMapper.GetTypeConventions() -> System.Collections.Generic.IReadOnlyDictionary!>! -static Dapper.FluentMap.FluentMapper.Initialize(System.Action! configure) -> void -static Dapper.FluentMap.FluentMapper.Runtime.get -> Dapper.FluentMap.FluentMapRuntime! -static Dapper.FluentMap.FluentMapper.Validate() -> void -static Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.Ignore(string! columnName) -> Dapper.FluentMap.Materialization.GeneratedMaterializerColumn! -static Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.Map(string! columnName, string! memberPath) -> Dapper.FluentMap.Materialization.GeneratedMaterializerColumn! -static Dapper.FluentMap.Materialization.GeneratedMaterializerColumn.Map(string! columnName, string! memberPath, System.Type! readConverterType, System.Type! readConverterDatabaseType, System.Type! readConverterPropertyType) -> Dapper.FluentMap.Materialization.GeneratedMaterializerColumn! -static Dapper.FluentMap.Naming.NamingPolicy.Custom(System.Func! transformer) -> Dapper.FluentMap.Naming.NamingPolicy! -static Dapper.FluentMap.Naming.NamingPolicy.Identity.get -> Dapper.FluentMap.Naming.NamingPolicy! -static Dapper.FluentMap.Naming.NamingPolicy.Prefix(string! prefix) -> Dapper.FluentMap.Naming.NamingPolicy! -static Dapper.FluentMap.Naming.NamingPolicy.SnakeCase.get -> Dapper.FluentMap.Naming.NamingPolicy! -static Dapper.FluentMap.Naming.NamingPolicy.Suffix(string! suffix) -> Dapper.FluentMap.Naming.NamingPolicy! -static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string! splitOn = "Id") -> System.Collections.Generic.IEnumerable! static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn) -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null) -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string? splitOn = null) -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null) -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task!>! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, System.Func! map, string! splitOn, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn) -> System.Collections.Generic.IEnumerable! +static Dapper.FluentMap.QueryMappedExtensions.QueryMapped(this System.Data.IDbConnection! connection, string! sql, System.Func! map, string! splitOn, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn) -> System.Threading.Tasks.Task!>! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, string! sql, System.Func! map, string! splitOn, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command, System.Func! map, string! splitOn) -> System.Threading.Tasks.Task!>! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, string? splitOn = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedSingle(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedSingle(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> TEntity! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedSingleAsync(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbuffered(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbuffered(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbuffered(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbuffered(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IAsyncEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, string! sql, System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, Dapper.CommandDefinition command) -> System.Collections.Generic.IAsyncEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMappedUnbufferedAsync(this System.Data.Common.DbConnection! connection, string! sql, System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerable! -static Dapper.FluentMap.QueryMappedExtensions.QueryMultipleMapped(this System.Data.IDbConnection! connection, Dapper.CommandDefinition command) -> Dapper.FluentMap.MappedGridReader! -static Dapper.FluentMap.QueryMappedExtensions.QueryMultipleMapped(this System.Data.IDbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> Dapper.FluentMap.MappedGridReader! -static Dapper.FluentMap.QueryMappedExtensions.QueryMultipleMappedAsync(this System.Data.Common.DbConnection! connection, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! -static Dapper.FluentMap.QueryMappedExtensions.QueryMultipleMappedAsync(this System.Data.Common.DbConnection! connection, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Dapper.FluentMap.TypeMaps.MultiTypeMap.MatchColumnNames(Dapper.FluentMap.Mapping.IPropertyMap! map, string! columnName) -> bool -static Dapper.FluentMap.TypeMaps.MultiTypeMap.TypePropertyMapCache.get -> System.Collections.Concurrent.ConcurrentDictionary! -static Dapper.FluentMap.Utils.FluentMapConfigurationExtensions.ApplyMapsFromAssemblies(this Dapper.FluentMap.Configuration.FluentMapConfiguration! configuration, params System.Reflection.Assembly![]! assemblies) -> void -static Dapper.FluentMap.Utils.ReflectionHelper.GetMemberInfo(System.Linq.Expressions.LambdaExpression! lambda) -> System.Reflection.MemberInfo! -static readonly Dapper.FluentMap.FluentMapper.EntityMaps -> System.Collections.Concurrent.ConcurrentDictionary! -static readonly Dapper.FluentMap.FluentMapper.TypeConventions -> System.Collections.Concurrent.ConcurrentDictionary!>! -static readonly Dapper.FluentMap.Mapping.PropertyConversionMetadata.Default -> Dapper.FluentMap.Mapping.PropertyConversionMetadata! -static readonly Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.Default -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! -static readonly Dapper.FluentMap.Mapping.PropertyPersistenceMetadata.Ignored -> Dapper.FluentMap.Mapping.PropertyPersistenceMetadata! -virtual Dapper.FluentMap.Mapping.ReadPropertyConverter.Invoke(TDatabase value) -> TProperty -virtual Dapper.FluentMap.Mapping.WritePropertyConverter.Invoke(TProperty value) -> TDatabase -virtual Dapper.FluentMap.Materialization.GeneratedRowMaterializer.Invoke(System.Data.IDataRecord! record) -> TEntity! +static Dapper.FluentMap.QueryMappedExtensions.QueryMappedAsync(this System.Data.IDbConnection! connection, string! sql, System.Func! map, string! splitOn, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! diff --git a/src/Dapper.FluentMap/QueryMappedExtensions.cs b/src/Dapper.FluentMap/QueryMappedExtensions.cs index 77f6836..2aab066 100644 --- a/src/Dapper.FluentMap/QueryMappedExtensions.cs +++ b/src/Dapper.FluentMap/QueryMappedExtensions.cs @@ -622,6 +622,10 @@ public static async Task QueryMappedSingleAsync< /// The composed rows. [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + [SuppressMessage( + "ApiDesign", + "RS0027:API with optional parameter(s) should have the most parameters among its public overloads", + Justification = "The established two-segment overload retains its optional splitOn contract; new three-segment overloads require two explicit boundaries.")] public static IEnumerable QueryMapped< [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, @@ -656,6 +660,10 @@ public static IEnumerable QueryMapped< /// [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + [SuppressMessage( + "ApiDesign", + "RS0027:API with optional parameter(s) should have the most parameters among its public overloads", + Justification = "The established two-segment overload retains its optional splitOn contract; new three-segment overloads require two explicit boundaries.")] public static IEnumerable QueryMapped< [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, @@ -697,11 +705,11 @@ public static IEnumerable QueryMapped< this IDbConnection connection, string sql, Func map, + string splitOn, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, - CommandType? commandType = null, - string? splitOn = null) + CommandType? commandType = null) where TFirst : class where TSecond : class where TThird : class @@ -731,7 +739,7 @@ public static IEnumerable QueryMapped< this IDbConnection connection, CommandDefinition command, Func map, - string? splitOn = null) + string splitOn) where TFirst : class where TSecond : class where TThird : class @@ -756,11 +764,11 @@ public static IEnumerable QueryMapped< this IDbConnection connection, string sql, Func map, + string splitOn, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, - CommandType? commandType = null, - string? splitOn = null) + CommandType? commandType = null) where TFirst : class where TSecond : class where TThird : class @@ -832,11 +840,11 @@ public static Task> QueryMappedAsync< this IDbConnection connection, string sql, Func map, + string splitOn, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, - string? splitOn = null, CancellationToken cancellationToken = default) where TFirst : class where TSecond : class @@ -903,11 +911,11 @@ public static Task> QueryMappedAsync< this IDbConnection connection, string sql, Func map, + string splitOn, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, - string? splitOn = null, CancellationToken cancellationToken = default) where TFirst : class where TSecond : class @@ -986,6 +994,10 @@ public static Task> QueryMappedAsync< /// The composed rows. [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + [SuppressMessage( + "ApiDesign", + "RS0027:API with optional parameter(s) should have the most parameters among its public overloads", + Justification = "The established profiled two-segment overload retains its optional splitOn contract; new three-segment overloads require two explicit boundaries.")] public static IEnumerable QueryMapped< [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, @@ -1024,6 +1036,10 @@ public static IEnumerable QueryMapped< /// [RequiresUnreferencedCode(QueryMappedApiAnnotations.RequiresUnreferencedCodeMessage)] [RequiresDynamicCode(QueryMappedApiAnnotations.RequiresDynamicCodeMessage)] + [SuppressMessage( + "ApiDesign", + "RS0027:API with optional parameter(s) should have the most parameters among its public overloads", + Justification = "The established profiled two-segment overload retains its optional splitOn contract; new three-segment overloads require two explicit boundaries.")] public static IEnumerable QueryMapped< [DynamicallyAccessedMembers(QueryMappedApiAnnotations.MaterializedEntityMemberTypes)] TFirst, @@ -1344,9 +1360,10 @@ private static IEnumerable ExecuteMappedSegments( throw new ArgumentNullException(nameof(runtime)); } + var boundaries = ParseSplitBoundaries(splitOn, entityTypes.Length); using (var reader = SqlMapper.ExecuteReader(connection, command)) { - var segments = CreateSegments(reader, splitOn, entityTypes.Length); + var segments = CreateSegments(reader, boundaries, entityTypes.Length); var materializers = CreateSegmentMaterializers(segments, entityTypes, profileTypes, runtime); var results = new List(); @@ -1378,13 +1395,14 @@ internal static async Task> ExecuteMappedSegmentsAsync(); - while (reader.Read()) + while (await ReadSegmentRowAsync(reader, command.CancellationToken).ConfigureAwait(false)) { results.Add(map(MaterializeSegments(segments, materializers))); } @@ -1393,6 +1411,18 @@ internal static async Task> ExecuteMappedSegmentsAsync ReadSegmentRowAsync(IDataReader reader, CancellationToken cancellationToken) + { + var dbReader = reader as DbDataReader; + if (dbReader != null) + { + return dbReader.ReadAsync(cancellationToken); + } + + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(reader.Read()); + } + internal static async Task ExecuteMultipleMappedAsync( DbConnection connection, CommandDefinition command, @@ -1412,7 +1442,7 @@ internal static async Task ExecuteMultipleMappedAsync( return new MappedGridReader(reader, runtime); } - private static SegmentDataRecord[] CreateSegments(IDataRecord reader, string splitOn, int segmentCount) + private static string[] ParseSplitBoundaries(string splitOn, int segmentCount) { if (string.IsNullOrWhiteSpace(splitOn)) { @@ -1441,6 +1471,14 @@ private static SegmentDataRecord[] CreateSegments(IDataRecord reader, string spl "The splitOn boundary '" + duplicateBoundary.Key + "' is duplicated and ambiguous."); } + return boundaries; + } + + private static SegmentDataRecord[] CreateSegments( + IDataRecord reader, + IReadOnlyList boundaries, + int segmentCount) + { var indexes = new List { 0 }; foreach (var boundary in boundaries) { diff --git a/test/Dapper.FluentMap.Analyzers.Tests/FluentMapConfigurationAnalyzerTests.cs b/test/Dapper.FluentMap.Analyzers.Tests/FluentMapConfigurationAnalyzerTests.cs index a4a76a3..ae73e7c 100644 --- a/test/Dapper.FluentMap.Analyzers.Tests/FluentMapConfigurationAnalyzerTests.cs +++ b/test/Dapper.FluentMap.Analyzers.Tests/FluentMapConfigurationAnalyzerTests.cs @@ -575,6 +575,44 @@ public CustomerMap() AssertDiagnosticLineContains(source, diagnostic, "ConstructUsing(c => c.Id, id => new Customer(id))"); } + [Fact] + public async Task InheritedConstructionStrategyShouldReportDfm016OnDerivedMap() + { + var source = @" +using Dapper.FluentMap.Mapping; + +public sealed class Customer +{ + public Customer(int id) { Id = id; } + public int Id { get; } +} + +public class CustomerMap : EntityMap +{ + public CustomerMap() + { + Map(c => c.Id).ToColumn(""customer_id""); + ConstructUsing(c => c.Id, id => new Customer(id)); + } +} + +public sealed class DerivedCustomerMap : CustomerMap +{ + public DerivedCustomerMap() + { + ConstructUsing(c => c.Id, id => new Customer(id)); + } +}"; + + var diagnostic = await GetSingleDiagnosticAsync( + source, + FluentMapConfigurationAnalyzer.DuplicateConstructionStrategyDiagnosticId); + + AssertDiagnostic(diagnostic, DiagnosticSeverity.Error, "more than one ConstructUsing"); + AssertDiagnosticLineContains(source, diagnostic, "ConstructUsing(c => c.Id, id => new Customer(id))"); + Assert.Equal(23, diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1); + } + [Fact] public async Task ValidMappingConfigurationShouldNotReportDiagnostics() { diff --git a/test/Dapper.FluentMap.Dommel.Tests/DommelPersistenceIntegrationTests.cs b/test/Dapper.FluentMap.Dommel.Tests/DommelPersistenceIntegrationTests.cs index 23d10c5..b19b396 100644 --- a/test/Dapper.FluentMap.Dommel.Tests/DommelPersistenceIntegrationTests.cs +++ b/test/Dapper.FluentMap.Dommel.Tests/DommelPersistenceIntegrationTests.cs @@ -443,27 +443,60 @@ public void ConverterOutputShouldThenUseDapperTypeHandler() SQLitePCL.Batteries_V2.Init(); SqlMapper.AddTypeHandler(new ConvertedCodeTypeHandler()); - FluentMapper.Initialize(config => + try { - config.AddMap(new TypeHandledWriteEntityMap()); - config.ForDommel(); - }); + FluentMapper.Initialize(config => + { + config.AddMap(new TypeHandledWriteEntityMap()); + config.ForDommel(); + }); - using (var connection = OpenConnection()) - { - connection.Execute(@" + using (var connection = OpenConnection()) + { + connection.Execute(@" CREATE TABLE type_handled_write_entities ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL );"); - var id = Convert.ToInt32(connection.InsertMapped(new TypeHandledWriteEntity { Code = "value" })); + var id = Convert.ToInt32(connection.InsertMapped(new TypeHandledWriteEntity { Code = "value" })); - Assert.Equal( - "handler:converted:value", - connection.QuerySingle( - "SELECT code FROM type_handled_write_entities WHERE id = @id;", - new { id })); + Assert.Equal( + "handler:converted:value", + connection.QuerySingle( + "SELECT code FROM type_handled_write_entities WHERE id = @id;", + new { id })); + } + } + finally + { + SqlMapper.ResetTypeHandlers(); + } + } + + [Fact] + public void MappedWritesShouldRejectEntitiesWithoutEligibleValues() + { + PreTest(); + SQLitePCL.Batteries_V2.Init(); + + FluentMapper.Initialize(config => + { + config.AddMap(new IdentityOnlyEntityMap()); + config.ForDommel(); + }); + + using (var connection = OpenConnection()) + { + var insertException = Assert.Throws(() => + connection.InsertMapped(new IdentityOnlyEntity())); + var updateException = Assert.Throws(() => + connection.UpdateMapped(new IdentityOnlyEntity { Id = 1 })); + + Assert.Contains(typeof(IdentityOnlyEntity).FullName, insertException.Message); + Assert.Contains("INSERT", insertException.Message); + Assert.Contains(typeof(IdentityOnlyEntity).FullName, updateException.Message); + Assert.Contains("UPDATE", updateException.Message); } } @@ -775,6 +808,20 @@ private sealed class TypeHandledWriteEntity public string Code { get; set; } } + private sealed class IdentityOnlyEntity + { + public int Id { get; set; } + } + + private sealed class IdentityOnlyEntityMap : DommelEntityMap + { + public IdentityOnlyEntityMap() + { + ToTable("identity_only_entities"); + Map(entity => entity.Id).ToColumn("id").IsIdentity(); + } + } + private sealed class TypeHandledWriteEntityMap : DommelEntityMap { public TypeHandledWriteEntityMap() diff --git a/test/Dapper.FluentMap.Tests/ExplicitConstructionStrategyTests.cs b/test/Dapper.FluentMap.Tests/ExplicitConstructionStrategyTests.cs index d4be789..ec8cda5 100644 --- a/test/Dapper.FluentMap.Tests/ExplicitConstructionStrategyTests.cs +++ b/test/Dapper.FluentMap.Tests/ExplicitConstructionStrategyTests.cs @@ -74,6 +74,25 @@ public void ValidationShouldRejectConstructionBindingWithoutActiveExplicitMap() } } + [Fact] + public void ValidationShouldRejectNestedConstructionBindingBeforeMaterialization() + { + FluentMapper.Reset(typeof(NestedFactoryEntity)); + + try + { + var exception = Assert.Throws(() => + FluentMapper.Initialize(configuration => configuration.AddMap(new NestedFactoryEntityMap()))); + + Assert.Contains("Nested.Name", exception.Message); + Assert.Contains("root property paths", exception.Message); + } + finally + { + FluentMapper.Reset(typeof(NestedFactoryEntity)); + } + } + [Fact] public void DuplicateConstructionStrategyShouldFailDuringMapConfiguration() { @@ -325,6 +344,35 @@ internal void ConfigureFour(Func fact } } + private sealed class NestedFactoryEntity + { + private NestedFactoryEntity(string name) + { + Nested = new NestedFactoryValue { Name = name }; + } + + public NestedFactoryValue Nested { get; } + + internal static NestedFactoryEntity Restore(string name) + { + return new NestedFactoryEntity(name); + } + } + + private sealed class NestedFactoryValue + { + public string Name { get; set; } + } + + private sealed class NestedFactoryEntityMap : EntityMap + { + public NestedFactoryEntityMap() + { + Map(entity => entity.Nested.Name).ToColumn("nested_name"); + ConstructUsing(entity => entity.Nested.Name, NestedFactoryEntity.Restore); + } + } + private sealed class DuplicateFactoryCustomerMap : FactoryCustomerMap { public DuplicateFactoryCustomerMap() diff --git a/test/Dapper.FluentMap.Tests/QueryMappedMultiMappingTests.cs b/test/Dapper.FluentMap.Tests/QueryMappedMultiMappingTests.cs index 1508839..6b49a38 100644 --- a/test/Dapper.FluentMap.Tests/QueryMappedMultiMappingTests.cs +++ b/test/Dapper.FluentMap.Tests/QueryMappedMultiMappingTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Dapper; using Dapper.FluentMap.Configuration; @@ -643,7 +644,14 @@ public void ThreeSegmentQueryShouldHonorPerSegmentProfilesAndIsolatedRuntime() [Trait("Category", "Integration")] public void ThreeSegmentGeneratedAndRuntimeMaterializationShouldBeEquivalent() { - var generatedRuntime = CreateThreeSegmentRuntime(registerGenerated: true); + var materializerCounts = new Dictionary(); + var generatedRuntime = CreateThreeSegmentRuntime( + registerGenerated: true, + materializedType => + { + materializerCounts.TryGetValue(materializedType, out var count); + materializerCounts[materializedType] = count + 1; + }); var reflectionRuntime = CreateThreeSegmentRuntime(registerGenerated: false); using (var connection = OpenConnection()) @@ -666,10 +674,15 @@ public void ThreeSegmentGeneratedAndRuntimeMaterializationShouldBeEquivalent() Assert.Equal(reflection.Order.Total, generated.Order.Total); Assert.Equal(reflection.Shipment.Id, generated.Shipment.Id); Assert.Equal(reflection.Shipment.Status, generated.Shipment.Status); + Assert.Equal(1, materializerCounts[typeof(JoinCustomer)]); + Assert.Equal(1, materializerCounts[typeof(JoinOrder)]); + Assert.Equal(1, materializerCounts[typeof(JoinShipment)]); } } - private static FluentMapRuntime CreateThreeSegmentRuntime(bool registerGenerated) + private static FluentMapRuntime CreateThreeSegmentRuntime( + bool registerGenerated, + Action generatedMaterialized = null) { var builder = new FluentMapConfigurationBuilder() .AddMap(new JoinCustomerMap()) @@ -685,10 +698,14 @@ private static FluentMapRuntime CreateThreeSegmentRuntime(bool registerGenerated GeneratedMaterializerColumn.Map("customer_id", nameof(JoinCustomer.Id)), GeneratedMaterializerColumn.Map("customer_name", nameof(JoinCustomer.Name)) }, - record => new JoinCustomer + record => { - Id = Convert.ToInt32(record.GetValue(0)), - Name = Convert.ToString(record.GetValue(1)) + generatedMaterialized?.Invoke(typeof(JoinCustomer)); + return new JoinCustomer + { + Id = Convert.ToInt32(record.GetValue(0)), + Name = Convert.ToString(record.GetValue(1)) + }; }) .AddGeneratedMaterializer( new[] @@ -696,10 +713,14 @@ private static FluentMapRuntime CreateThreeSegmentRuntime(bool registerGenerated GeneratedMaterializerColumn.Map("order_id", nameof(JoinOrder.Id)), GeneratedMaterializerColumn.Map("total", nameof(JoinOrder.Total)) }, - record => new JoinOrder + record => { - Id = Convert.ToInt32(record.GetValue(0)), - Total = Convert.ToDecimal(record.GetValue(1)) + generatedMaterialized?.Invoke(typeof(JoinOrder)); + return new JoinOrder + { + Id = Convert.ToInt32(record.GetValue(0)), + Total = Convert.ToDecimal(record.GetValue(1)) + }; }) .AddGeneratedMaterializer( new[] @@ -707,10 +728,14 @@ private static FluentMapRuntime CreateThreeSegmentRuntime(bool registerGenerated GeneratedMaterializerColumn.Map("shipment_id", nameof(JoinShipment.Id)), GeneratedMaterializerColumn.Map("shipment_status", nameof(JoinShipment.Status)) }, - record => new JoinShipment + record => { - Id = Convert.ToInt32(record.GetValue(0)), - Status = Convert.ToString(record.GetValue(1)) + generatedMaterialized?.Invoke(typeof(JoinShipment)); + return new JoinShipment + { + Id = Convert.ToInt32(record.GetValue(0)), + Status = Convert.ToString(record.GetValue(1)) + }; }); } From 7e552ee108740b409c6d9918b1cc2ab7b614d1a9 Mon Sep 17 00:00:00 2001 From: Rodrigo de Oliveira Date: Wed, 30 Sep 2026 15:39:52 -0300 Subject: [PATCH 9/9] docs: clarify generated construction detection --- USAGE.md | 2 +- USAGE.pt-BR.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/USAGE.md b/USAGE.md index 3e5b1c5..db2f235 100644 --- a/USAGE.md +++ b/USAGE.md @@ -156,7 +156,7 @@ public CustomerMap() } ``` -`ConstructUsing` supports one to four explicitly mapped root-property values, validates every binding, and is preserved by isolated runtimes. Nested property paths are rejected during configuration because explicit factories currently bind only the root materialization node. Delegate factories are runtime-materialization strategies: the source generator registers the map but reports `DFM011` and does not emit a generated materializer. Strict generated mode therefore rejects that shape instead of silently using the factory through reflection. +`ConstructUsing` supports one to four explicitly mapped root-property values, validates every binding, and is preserved by isolated runtimes. Nested property paths are rejected during configuration because explicit factories currently bind only the root materialization node. Call `ConstructUsing` directly from the map constructor: the source generator analyzes constructor invocations and does not follow helper-method calls. Delegate factories are runtime-materialization strategies: the source generator registers the map but reports `DFM011` and does not emit a generated materializer. Strict generated mode therefore rejects that shape instead of silently using the factory through reflection. ## Mapping Profiles diff --git a/USAGE.pt-BR.md b/USAGE.pt-BR.md index 72aabc8..0126998 100644 --- a/USAGE.pt-BR.md +++ b/USAGE.pt-BR.md @@ -156,7 +156,7 @@ public CustomerMap() } ``` -`ConstructUsing` aceita de um a quatro valores de propriedades raiz explicitamente mapeadas, valida cada binding e é preservado por runtimes isolados. Caminhos de propriedades aninhadas são rejeitados durante a configuração porque factories explícitas atualmente vinculam apenas o nó raiz de materialização. Factories delegate são estratégias de materialização em runtime: o source generator registra o map, mas reporta `DFM011` e não emite materializador gerado. Portanto, o modo strict generated rejeita esse shape sem fallback silencioso. +`ConstructUsing` aceita de um a quatro valores de propriedades raiz explicitamente mapeadas, valida cada binding e é preservado por runtimes isolados. Caminhos de propriedades aninhadas são rejeitados durante a configuração porque factories explícitas atualmente vinculam apenas o nó raiz de materialização. Chame `ConstructUsing` diretamente no construtor do map: o source generator analisa as invocações do construtor e não segue chamadas a métodos auxiliares. Factories delegate são estratégias de materialização em runtime: o source generator registra o map, mas reporta `DFM011` e não emite materializador gerado. Portanto, o modo strict generated rejeita esse shape sem fallback silencioso. ## Mapping Profiles