diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..19273c5e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug, needs-triage +assignees: '' + +--- + +**Check your library version, and try updating** +To help, we're going to need to know your library version. If it isn't the latest: *go do that* - it might +fix the problem, and even if it doesn't: you're going to need to update if we find a problem and fix it, +so you might as well get ready for that now. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected and actual behavior** +A clear and concise description of what you expected to happen, and what actually happens. + +**Additional context** +Add any other context about the problem here: +- what DB backend (and version) are you using, if relevant? +- what ADO.NET provider (and version) are you using, if relevant? +- what OS and .NET runtime (and version) are you using, if relevant? diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 000000000..5e3b83b36 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,43 @@ +name: "CLA Assistant" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened,closed,synchronize] + +# explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + CLAAssistant: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + uses: contributor-assistant/github-action@v2.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # the below token should have repo scope and must be manually added by you in the repository's secret + # This token is required only if you have configured to store the signatures in a remote repository/organization + PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + with: + path-to-signatures: 'signatures/version1/cla.json' + path-to-document: 'https://raw.githubusercontent.com/DapperLib/Dapper/main/NonCLA.md' # e.g. a CLA or a DCO document + # branch should not be protected + branch: 'main' + # allowlist: user1,bot* + + # the followings are the optional inputs - If the optional inputs are not given, then default values will be taken + #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) + #remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository) + #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' + #signed-commit-message: 'For example: $contributorName has signed the CLA in $owner/$repo#$pullRequestNo' + #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' + #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' + #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' + #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) + #use-dco-flag: true - If you are using DCO instead of CLA diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a31c48f20..3145873c1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,10 +40,14 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v1 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true - name: Dapper Tests - run: dotnet test tests/Dapper.Tests/Dapper.Tests.csproj -c Release --logger GitHubActions /p:CI=true + run: dotnet test tests/Dapper.Tests/Dapper.Tests.csproj -c Release --logger GitHubActions -p:CI=true -p:TestTfmsInParallel=false env: MySqlConnectionString: Server=localhost;Port=${{ job.services.mysql.ports[3306] }};Uid=root;Pwd=root;Database=test;Allow User Variables=true OLEDBConnectionString: Provider=SQLOLEDB;Server=tcp:localhost,${{ job.services.sqlserver.ports[1433] }};Database=tempdb;User Id=sa;Password=Password.; diff --git a/Build.csproj b/Build.csproj index f2f2d0d6b..c2ed16c13 100644 --- a/Build.csproj +++ b/Build.csproj @@ -1,7 +1,7 @@ - + \ No newline at end of file diff --git a/Dapper.ProviderTools/Dapper.ProviderTools.csproj b/Dapper.ProviderTools/Dapper.ProviderTools.csproj index 61e8b4c58..176887317 100644 --- a/Dapper.ProviderTools/Dapper.ProviderTools.csproj +++ b/Dapper.ProviderTools/Dapper.ProviderTools.csproj @@ -5,7 +5,7 @@ Codestin Search App Provider-agnostic ADO.NET helper utilities Marc Gravell - net461;netstandard2.0;net5.0 + net461;netstandard2.0;net8.0 true enable diff --git a/Dapper.SqlBuilder/Dapper.SqlBuilder.csproj b/Dapper.SqlBuilder/Dapper.SqlBuilder.csproj index b597bd9eb..a455a5941 100644 --- a/Dapper.SqlBuilder/Dapper.SqlBuilder.csproj +++ b/Dapper.SqlBuilder/Dapper.SqlBuilder.csproj @@ -5,7 +5,7 @@ Codestin Search App The Dapper SqlBuilder component, for building SQL queries dynamically. Sam Saffron, Johan Danforth - net461;netstandard2.0;net5.0 + net461;netstandard2.0;net8.0 false false enable @@ -19,7 +19,8 @@ - + + diff --git a/Dapper.SqlBuilder/Readme.md b/Dapper.SqlBuilder/Readme.md index ea69d9b80..5a0968f9f 100644 --- a/Dapper.SqlBuilder/Readme.md +++ b/Dapper.SqlBuilder/Readme.md @@ -85,10 +85,16 @@ var count = conn.ExecuteScalar(countTemplate.RawSql, countTemplate.Paramete Limitations and caveats -------- -OrWhere use `and` not `or` to concat sql problem +### Combining the Where and OrWhere methods -[Issue 647](https://github.com/DapperLib/Dapper/issues/647) +The OrWhere method currently groups all `and` and `or` clauses by type, +then join the groups with `and` or `or` depending on the first call. +This may result in possibly unexpected outcomes. +See also [issue 647](https://github.com/DapperLib/Dapper/issues/647). +#### Example Where first + +When providing the following clauses ```csharp sql.Where("a = @a1"); sql.OrWhere("b = @b1"); @@ -97,11 +103,26 @@ sql.OrWhere("b = @b2"); ``` SqlBuilder will generate sql -```sql= -a = @a1 AND b = @b1 AND a = @a2 AND b = @b2 +```sql +a = @a1 AND a = @a2 AND ( b = @b1 OR b = @b2 ) ``` -not +and not say ```sql a = @a1 OR b = @b1 AND a = @a2 OR b = @b2 ``` + +#### Example OrWhere first + +When providing the following clauses +```csharp +sql.OrWhere("b = @b1"); +sql.Where("a = @a1"); +sql.OrWhere("b = @b2"); +sql.Where("a = @a2"); +``` + +SqlBuilder will generate sql +```sql +a = @a1 OR a = @a2 OR ( b = @b1 OR b = @b2 ) +``` diff --git a/Dapper.SqlBuilder/SqlBuilder.cs b/Dapper.SqlBuilder/SqlBuilder.cs index 5726fdc6e..cb45d0582 100644 --- a/Dapper.SqlBuilder/SqlBuilder.cs +++ b/Dapper.SqlBuilder/SqlBuilder.cs @@ -10,7 +10,7 @@ public class SqlBuilder private readonly Dictionary _data = new Dictionary(); private int _seq; - private class Clause + private sealed class Clause { public Clause(string sql, object? parameters, bool isInclusive) { @@ -23,7 +23,7 @@ public Clause(string sql, object? parameters, bool isInclusive) public bool IsInclusive { get; } } - private class Clauses : List + private sealed class Clauses : List { private readonly string _joiner, _prefix, _postfix; @@ -36,11 +36,9 @@ public Clauses(string joiner, string prefix = "", string postfix = "") public string ResolveClauses(DynamicParameters p) { - foreach (var item in this) - { - p.AddDynamicParams(item.Parameters); - } - return this.Any(a => a.IsInclusive) + ForEach(item => p.AddDynamicParams(item.Parameters)); + + return Exists(a => a.IsInclusive) ? _prefix + string.Join(_joiner, this.Where(a => !a.IsInclusive) diff --git a/Dapper.StrongName/Dapper.StrongName.csproj b/Dapper.StrongName/Dapper.StrongName.csproj index ef28e32b4..705385fd6 100644 --- a/Dapper.StrongName/Dapper.StrongName.csproj +++ b/Dapper.StrongName/Dapper.StrongName.csproj @@ -3,9 +3,9 @@ Dapper.StrongName orm;sql;micro-orm Codestin Search App - A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc.. + A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc. Major Sponsor: Dapper Plus from ZZZ Projects. Sam Saffron;Marc Gravell;Nick Craver - net461;netstandard2.0;net5.0;net7.0 + net461;netstandard2.0;net8.0;net10.0 true true enable @@ -17,7 +17,14 @@ + + + + + + + diff --git a/Dapper.sln b/Dapper.sln.old similarity index 92% rename from Dapper.sln rename to Dapper.sln.old index 7a610ba6a..e324cc009 100644 --- a/Dapper.sln +++ b/Dapper.sln.old @@ -1,116 +1,121 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.7.33906.173 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A34907DF-958A-4E4C-8491-84CF303FD13E}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - appveyor.yml = appveyor.yml - Build.csproj = Build.csproj - build.ps1 = build.ps1 - Dapper.png = Dapper.png - Directory.Build.props = Directory.Build.props - Directory.Packages.props = Directory.Packages.props - global.json = global.json - docs\index.md = docs\index.md - License.txt = License.txt - nuget.config = nuget.config - Readme.md = Readme.md - version.json = version.json - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper", "Dapper\Dapper.csproj", "{FAC24C3F-68F9-4247-A4B9-21D487E99275}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.StrongName", "Dapper.StrongName\Dapper.StrongName.csproj", "{549C51A1-222B-4E12-96F1-3AEFF45A7709}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests", "tests\Dapper.Tests\Dapper.Tests.csproj", "{052C0817-DB26-4925-8929-8C5E42D148D5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.EntityFramework", "Dapper.EntityFramework\Dapper.EntityFramework.csproj", "{BE401F7B-8611-4A1E-AEAA-5CB700128C16}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.SqlBuilder", "Dapper.SqlBuilder\Dapper.SqlBuilder.csproj", "{196928F0-7052-4585-90E8-817BD720F5E3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Rainbow", "Dapper.Rainbow\Dapper.Rainbow.csproj", "{8A74F0B6-188F-45D2-8A4B-51E4F211805A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4E956F6B-6BD8-46F5-BC85-49292FF8F9AB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{568BD46C-1C65-4D44-870C-12CD72563262}" - ProjectSection(SolutionItems) = preProject - tests\Directory.Build.props = tests\Directory.Build.props - tests\Directory.Build.targets = tests\Directory.Build.targets - tests\docker-compose.yml = tests\docker-compose.yml - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.EntityFramework.StrongName", "Dapper.EntityFramework.StrongName\Dapper.EntityFramework.StrongName.csproj", "{39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests.Performance", "benchmarks\Dapper.Tests.Performance\Dapper.Tests.Performance.csproj", "{F017075A-2969-4A8E-8971-26F154EB420F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.ProviderTools", "Dapper.ProviderTools\Dapper.ProviderTools.csproj", "{B06DB435-0C74-4BD3-BC97-52AF7CF9916B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{9D960D4D-80A2-4DAC-B386-8F4235EC73E6}" - ProjectSection(SolutionItems) = preProject - docs\index.md = docs\index.md - docs\readme.md = docs\readme.md - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Release|Any CPU.Build.0 = Release|Any CPU - {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Debug|Any CPU.Build.0 = Debug|Any CPU - {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Release|Any CPU.ActiveCfg = Release|Any CPU - {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Release|Any CPU.Build.0 = Release|Any CPU - {052C0817-DB26-4925-8929-8C5E42D148D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {052C0817-DB26-4925-8929-8C5E42D148D5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {052C0817-DB26-4925-8929-8C5E42D148D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {052C0817-DB26-4925-8929-8C5E42D148D5}.Release|Any CPU.Build.0 = Release|Any CPU - {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Release|Any CPU.Build.0 = Release|Any CPU - {196928F0-7052-4585-90E8-817BD720F5E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {196928F0-7052-4585-90E8-817BD720F5E3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {196928F0-7052-4585-90E8-817BD720F5E3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {196928F0-7052-4585-90E8-817BD720F5E3}.Release|Any CPU.Build.0 = Release|Any CPU - {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Release|Any CPU.Build.0 = Release|Any CPU - {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Release|Any CPU.Build.0 = Release|Any CPU - {F017075A-2969-4A8E-8971-26F154EB420F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F017075A-2969-4A8E-8971-26F154EB420F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F017075A-2969-4A8E-8971-26F154EB420F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F017075A-2969-4A8E-8971-26F154EB420F}.Release|Any CPU.Build.0 = Release|Any CPU - {B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {FAC24C3F-68F9-4247-A4B9-21D487E99275} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {549C51A1-222B-4E12-96F1-3AEFF45A7709} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {052C0817-DB26-4925-8929-8C5E42D148D5} = {568BD46C-1C65-4D44-870C-12CD72563262} - {BE401F7B-8611-4A1E-AEAA-5CB700128C16} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {196928F0-7052-4585-90E8-817BD720F5E3} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {8A74F0B6-188F-45D2-8A4B-51E4F211805A} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {F017075A-2969-4A8E-8971-26F154EB420F} = {568BD46C-1C65-4D44-870C-12CD72563262} - {B06DB435-0C74-4BD3-BC97-52AF7CF9916B} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {928A4226-96F3-409A-8A83-9E7444488710} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.33906.173 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A34907DF-958A-4E4C-8491-84CF303FD13E}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + appveyor.yml = appveyor.yml + Build.csproj = Build.csproj + build.ps1 = build.ps1 + Dapper.png = Dapper.png + Directory.Build.props = Directory.Build.props + Directory.Packages.props = Directory.Packages.props + global.json = global.json + docs\index.md = docs\index.md + License.txt = License.txt + .github\workflows\main.yml = .github\workflows\main.yml + NonCLA.md = NonCLA.md + nuget.config = nuget.config + Readme.md = Readme.md + version.json = version.json + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper", "Dapper\Dapper.csproj", "{FAC24C3F-68F9-4247-A4B9-21D487E99275}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.StrongName", "Dapper.StrongName\Dapper.StrongName.csproj", "{549C51A1-222B-4E12-96F1-3AEFF45A7709}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests", "tests\Dapper.Tests\Dapper.Tests.csproj", "{052C0817-DB26-4925-8929-8C5E42D148D5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.EntityFramework", "Dapper.EntityFramework\Dapper.EntityFramework.csproj", "{BE401F7B-8611-4A1E-AEAA-5CB700128C16}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.SqlBuilder", "Dapper.SqlBuilder\Dapper.SqlBuilder.csproj", "{196928F0-7052-4585-90E8-817BD720F5E3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Rainbow", "Dapper.Rainbow\Dapper.Rainbow.csproj", "{8A74F0B6-188F-45D2-8A4B-51E4F211805A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4E956F6B-6BD8-46F5-BC85-49292FF8F9AB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{568BD46C-1C65-4D44-870C-12CD72563262}" + ProjectSection(SolutionItems) = preProject + tests\Directory.Build.props = tests\Directory.Build.props + tests\Directory.Build.targets = tests\Directory.Build.targets + tests\docker-compose.yml = tests\docker-compose.yml + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.EntityFramework.StrongName", "Dapper.EntityFramework.StrongName\Dapper.EntityFramework.StrongName.csproj", "{39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests.Performance", "benchmarks\Dapper.Tests.Performance\Dapper.Tests.Performance.csproj", "{F017075A-2969-4A8E-8971-26F154EB420F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.ProviderTools", "Dapper.ProviderTools\Dapper.ProviderTools.csproj", "{B06DB435-0C74-4BD3-BC97-52AF7CF9916B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{9D960D4D-80A2-4DAC-B386-8F4235EC73E6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "docs", "docs\docs.csproj", "{C2F722AC-B2D4-4E97-AF8E-C036B3D9211F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Release|Any CPU.Build.0 = Release|Any CPU + {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Debug|Any CPU.Build.0 = Debug|Any CPU + {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Release|Any CPU.ActiveCfg = Release|Any CPU + {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Release|Any CPU.Build.0 = Release|Any CPU + {052C0817-DB26-4925-8929-8C5E42D148D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {052C0817-DB26-4925-8929-8C5E42D148D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {052C0817-DB26-4925-8929-8C5E42D148D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {052C0817-DB26-4925-8929-8C5E42D148D5}.Release|Any CPU.Build.0 = Release|Any CPU + {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Release|Any CPU.Build.0 = Release|Any CPU + {196928F0-7052-4585-90E8-817BD720F5E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {196928F0-7052-4585-90E8-817BD720F5E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {196928F0-7052-4585-90E8-817BD720F5E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {196928F0-7052-4585-90E8-817BD720F5E3}.Release|Any CPU.Build.0 = Release|Any CPU + {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Release|Any CPU.Build.0 = Release|Any CPU + {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Release|Any CPU.Build.0 = Release|Any CPU + {F017075A-2969-4A8E-8971-26F154EB420F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F017075A-2969-4A8E-8971-26F154EB420F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F017075A-2969-4A8E-8971-26F154EB420F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F017075A-2969-4A8E-8971-26F154EB420F}.Release|Any CPU.Build.0 = Release|Any CPU + {B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Release|Any CPU.Build.0 = Release|Any CPU + {C2F722AC-B2D4-4E97-AF8E-C036B3D9211F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2F722AC-B2D4-4E97-AF8E-C036B3D9211F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2F722AC-B2D4-4E97-AF8E-C036B3D9211F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2F722AC-B2D4-4E97-AF8E-C036B3D9211F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {FAC24C3F-68F9-4247-A4B9-21D487E99275} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {549C51A1-222B-4E12-96F1-3AEFF45A7709} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {052C0817-DB26-4925-8929-8C5E42D148D5} = {568BD46C-1C65-4D44-870C-12CD72563262} + {BE401F7B-8611-4A1E-AEAA-5CB700128C16} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {196928F0-7052-4585-90E8-817BD720F5E3} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {8A74F0B6-188F-45D2-8A4B-51E4F211805A} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {F017075A-2969-4A8E-8971-26F154EB420F} = {568BD46C-1C65-4D44-870C-12CD72563262} + {B06DB435-0C74-4BD3-BC97-52AF7CF9916B} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {C2F722AC-B2D4-4E97-AF8E-C036B3D9211F} = {9D960D4D-80A2-4DAC-B386-8F4235EC73E6} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {928A4226-96F3-409A-8A83-9E7444488710} + EndGlobalSection +EndGlobal diff --git a/Dapper.slnx b/Dapper.slnx new file mode 100644 index 000000000..674239170 --- /dev/null +++ b/Dapper.slnx @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dapper/CommandDefinition.cs b/Dapper/CommandDefinition.cs index cc117c304..19963ba67 100644 --- a/Dapper/CommandDefinition.cs +++ b/Dapper/CommandDefinition.cs @@ -11,16 +11,9 @@ namespace Dapper /// public readonly struct CommandDefinition { - internal static CommandDefinition ForCallback(object? parameters) + internal static CommandDefinition ForCallback(object? parameters, CommandFlags flags) { - if (parameters is DynamicParameters) - { - return new CommandDefinition(parameters); - } - else - { - return default; - } + return new CommandDefinition(parameters is DynamicParameters ? parameters : null, flags); } internal void OnCompleted() @@ -113,9 +106,10 @@ internal static CommandType InferCommandType(string sql) return System.Data.CommandType.StoredProcedure; } - private CommandDefinition(object? parameters) : this() + private CommandDefinition(object? parameters, CommandFlags flags) : this() { Parameters = parameters; + Flags = flags; CommandText = ""; } diff --git a/Dapper/Dapper.csproj b/Dapper/Dapper.csproj index 8fc9c65f8..d9518dc45 100644 --- a/Dapper/Dapper.csproj +++ b/Dapper/Dapper.csproj @@ -3,9 +3,9 @@ Dapper Codestin Search App orm;sql;micro-orm - A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc.. + A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc. Major Sponsor: Dapper Plus from ZZZ Projects. Sam Saffron;Marc Gravell;Nick Craver - net461;netstandard2.0;net5.0;net7.0 + net461;netstandard2.0;net8.0;net10.0 enable true @@ -24,7 +24,14 @@ + + + + + + + diff --git a/Dapper/DefaultTypeMap.cs b/Dapper/DefaultTypeMap.cs index 98029741a..c2f691883 100644 --- a/Dapper/DefaultTypeMap.cs +++ b/Dapper/DefaultTypeMap.cs @@ -10,7 +10,7 @@ namespace Dapper /// public sealed class DefaultTypeMap : SqlMapper.ITypeMap { - private readonly List _fields; + private readonly FieldInfo[] _fields; private readonly Type _type; /// @@ -42,7 +42,7 @@ internal static MethodInfo GetPropertySetterOrThrow(PropertyInfo propertyInfo, T BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, Type.DefaultBinder, propertyInfo.PropertyType, - propertyInfo.GetIndexParameters().Select(p => p.ParameterType).ToArray(), + Array.ConvertAll(propertyInfo.GetIndexParameters(), p => p.ParameterType), null)!.GetSetMethod(true); } @@ -54,9 +54,9 @@ internal static List GetSettableProps(Type t) .ToList(); } - internal static List GetSettableFields(Type t) + private static FieldInfo[] GetSettableFields(Type t) { - return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToList(); + return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); } /// @@ -156,20 +156,20 @@ public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, // preference order is: // exact match over underscore match, exact case over wrong case, backing fields over regular fields, match-inc-underscores over match-exc-underscores - var field = _fields.Find(p => string.Equals(p.Name, columnName, StringComparison.Ordinal)) - ?? _fields.Find(p => string.Equals(p.Name, backingFieldName, StringComparison.Ordinal)) - ?? _fields.Find(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)) - ?? _fields.Find(p => string.Equals(p.Name, backingFieldName, StringComparison.OrdinalIgnoreCase)); + var field = Array.Find(_fields, p => string.Equals(p.Name, columnName, StringComparison.Ordinal)) + ?? Array.Find(_fields, p => string.Equals(p.Name, backingFieldName, StringComparison.Ordinal)) + ?? Array.Find(_fields, p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)) + ?? Array.Find(_fields, p => string.Equals(p.Name, backingFieldName, StringComparison.OrdinalIgnoreCase)); if (field is null && MatchNamesWithUnderscores) { var effectiveColumnName = columnName.Replace("_", ""); backingFieldName = "<" + effectiveColumnName + ">k__BackingField"; - field = _fields.Find(p => string.Equals(p.Name, effectiveColumnName, StringComparison.Ordinal)) - ?? _fields.Find(p => string.Equals(p.Name, backingFieldName, StringComparison.Ordinal)) - ?? _fields.Find(p => string.Equals(p.Name, effectiveColumnName, StringComparison.OrdinalIgnoreCase)) - ?? _fields.Find(p => string.Equals(p.Name, backingFieldName, StringComparison.OrdinalIgnoreCase)); + field = Array.Find(_fields, p => string.Equals(p.Name, effectiveColumnName, StringComparison.Ordinal)) + ?? Array.Find(_fields, p => string.Equals(p.Name, backingFieldName, StringComparison.Ordinal)) + ?? Array.Find(_fields, p => string.Equals(p.Name, effectiveColumnName, StringComparison.OrdinalIgnoreCase)) + ?? Array.Find(_fields, p => string.Equals(p.Name, backingFieldName, StringComparison.OrdinalIgnoreCase)); } if (field is not null) diff --git a/Dapper/DynamicParameters.cs b/Dapper/DynamicParameters.cs index f6708b5ce..1feb4d020 100644 --- a/Dapper/DynamicParameters.cs +++ b/Dapper/DynamicParameters.cs @@ -62,10 +62,7 @@ public void AddDynamicParams(object? param) if (subDynamic.templates is not null) { templates ??= new List(); - foreach (var t in subDynamic.templates) - { - templates.Add(t); - } + templates.AddRange(subDynamic.templates); } } else @@ -212,14 +209,7 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) } // Now that the parameters are added to the command, let's place our output callbacks - var tmp = outputCallbacks; - if (tmp is not null) - { - foreach (var generator in tmp) - { - generator(); - } - } + outputCallbacks?.ForEach(generator => generator()); } foreach (var param in parameters.Values) diff --git a/Dapper/FeatureSupport.cs b/Dapper/FeatureSupport.cs index 4d7d77e2e..f00c38ed3 100644 --- a/Dapper/FeatureSupport.cs +++ b/Dapper/FeatureSupport.cs @@ -6,7 +6,7 @@ namespace Dapper /// /// Handles variances in features per DBMS /// - internal class FeatureSupport + internal sealed class FeatureSupport { private static readonly FeatureSupport Default = new FeatureSupport(false), diff --git a/Dapper/PublicAPI.Shipped.txt b/Dapper/PublicAPI.Shipped.txt index c1b08ea99..456aa8bb1 100644 --- a/Dapper/PublicAPI.Shipped.txt +++ b/Dapper/PublicAPI.Shipped.txt @@ -69,6 +69,7 @@ Dapper.SqlMapper.GridReader.CancellationToken.get -> System.Threading.Cancellati Dapper.SqlMapper.GridReader.Command.get -> System.Data.IDbCommand! Dapper.SqlMapper.GridReader.Command.set -> void Dapper.SqlMapper.GridReader.Dispose() -> void +Dapper.SqlMapper.GridReader.DisposeAsync() -> System.Threading.Tasks.ValueTask Dapper.SqlMapper.GridReader.GridReader(System.Data.IDbCommand! command, System.Data.Common.DbDataReader! reader, Dapper.SqlMapper.Identity? identity, System.Action? onCompleted = null, object? state = null, bool addToCache = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void Dapper.SqlMapper.GridReader.IsConsumed.get -> bool Dapper.SqlMapper.GridReader.OnAfterGrid(int index) -> void @@ -112,6 +113,8 @@ Dapper.SqlMapper.GridReader.ReadSingleOrDefault() -> T? Dapper.SqlMapper.GridReader.ReadSingleOrDefaultAsync() -> System.Threading.Tasks.Task! Dapper.SqlMapper.GridReader.ReadSingleOrDefaultAsync(System.Type! type) -> System.Threading.Tasks.Task! Dapper.SqlMapper.GridReader.ReadSingleOrDefaultAsync() -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable! +Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable! Dapper.SqlMapper.GridReader.ResultIndex.get -> int Dapper.SqlMapper.ICustomQueryParameter Dapper.SqlMapper.ICustomQueryParameter.AddParameter(System.Data.IDbCommand! command, string! name) -> void @@ -295,6 +298,8 @@ static Dapper.SqlMapper.QuerySingleOrDefaultAsync(this System.Data.IDbConnection static Dapper.SqlMapper.QuerySingleOrDefaultAsync(this System.Data.IDbConnection! cnn, System.Type! type, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! static Dapper.SqlMapper.QuerySingleOrDefaultAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! static Dapper.SqlMapper.QuerySingleOrDefaultAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable! static Dapper.SqlMapper.ReadChar(object! value) -> char static Dapper.SqlMapper.ReadNullableChar(object! value) -> char? static Dapper.SqlMapper.RemoveTypeMap(System.Type! type) -> void diff --git a/Dapper/PublicAPI.Unshipped.txt b/Dapper/PublicAPI.Unshipped.txt index 91b0e1a43..cf9343ccf 100644 --- a/Dapper/PublicAPI.Unshipped.txt +++ b/Dapper/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ -#nullable enable \ No newline at end of file +#nullable enable +static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.get -> bool +static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.set -> void \ No newline at end of file diff --git a/Dapper/PublicAPI/net5.0/PublicAPI.Shipped.txt b/Dapper/PublicAPI/net5.0/PublicAPI.Shipped.txt deleted file mode 100644 index 5da46e604..000000000 --- a/Dapper/PublicAPI/net5.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,6 +0,0 @@ -#nullable enable -Dapper.SqlMapper.GridReader.DisposeAsync() -> System.Threading.Tasks.ValueTask -Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable! -Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable! -static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable! -static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable! \ No newline at end of file diff --git a/Dapper/PublicAPI/net7.0/PublicAPI.Shipped.txt b/Dapper/PublicAPI/net7.0/PublicAPI.Shipped.txt deleted file mode 100644 index 5da46e604..000000000 --- a/Dapper/PublicAPI/net7.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,6 +0,0 @@ -#nullable enable -Dapper.SqlMapper.GridReader.DisposeAsync() -> System.Threading.Tasks.ValueTask -Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable! -Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable! -static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable! -static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable! \ No newline at end of file diff --git a/Dapper/PublicAPI/net7.0/PublicAPI.Unshipped.txt b/Dapper/PublicAPI/net7.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 91b0e1a43..000000000 --- a/Dapper/PublicAPI/net7.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable \ No newline at end of file diff --git a/Dapper/PublicAPI/net8.0/PublicAPI.Shipped.txt b/Dapper/PublicAPI/net8.0/PublicAPI.Shipped.txt new file mode 100644 index 000000000..ab058de62 --- /dev/null +++ b/Dapper/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Dapper/PublicAPI/net5.0/PublicAPI.Unshipped.txt b/Dapper/PublicAPI/net8.0/PublicAPI.Unshipped.txt similarity index 100% rename from Dapper/PublicAPI/net5.0/PublicAPI.Unshipped.txt rename to Dapper/PublicAPI/net8.0/PublicAPI.Unshipped.txt diff --git a/Dapper/SqlMapper.Async.cs b/Dapper/SqlMapper.Async.cs index 6fb8ca4c2..eade08cb2 100644 --- a/Dapper/SqlMapper.Async.cs +++ b/Dapper/SqlMapper.Async.cs @@ -503,6 +503,7 @@ private static async Task QueryRowAsync(this IDbConnection cnn, Row row, T ThrowZeroRows(row); } while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore result sets after the first */ } + command.OnCompleted(); return result; } finally @@ -939,7 +940,7 @@ private static async Task> MultiMapAsync(null, CommandDefinition.ForCallback(command.Parameters), map, splitOn, reader, identity, true); + var results = MultiMapImpl(null, CommandDefinition.ForCallback(command.Parameters, command.Flags), map, splitOn, reader, identity, true); return command.Buffered ? results.ToList() : results; } finally @@ -1249,7 +1250,6 @@ private static async Task ExecuteWrappedReaderImplAsync(IDbConnect return Parse(result); } -#if NET5_0_OR_GREATER /// /// Execute a query asynchronously using . /// @@ -1333,12 +1333,20 @@ static async IAsyncEnumerable Impl(IDbConnection cnn, Type effectiveType, Com { if (reader is not null) { + if (!reader.IsClosed) + { + try { cmd?.Cancel(); } + catch { /* don't spoil any existing exception */ } + } +#if NET5_0_OR_GREATER await reader.DisposeAsync(); +#else + reader.Dispose(); +#endif } if (wasClosed) cnn.Close(); } } } -#endif } } diff --git a/Dapper/SqlMapper.CacheInfo.cs b/Dapper/SqlMapper.CacheInfo.cs index d235beb6f..69edc4eea 100644 --- a/Dapper/SqlMapper.CacheInfo.cs +++ b/Dapper/SqlMapper.CacheInfo.cs @@ -7,7 +7,7 @@ namespace Dapper { public static partial class SqlMapper { - private class CacheInfo + private sealed class CacheInfo { public DeserializerState Deserializer { get; set; } public Func[]? OtherDeserializers { get; set; } diff --git a/Dapper/SqlMapper.DapperRowMetaObject.cs b/Dapper/SqlMapper.DapperRowMetaObject.cs index 75e7a15a6..7b5350dfb 100644 --- a/Dapper/SqlMapper.DapperRowMetaObject.cs +++ b/Dapper/SqlMapper.DapperRowMetaObject.cs @@ -81,7 +81,7 @@ public override System.Dynamic.DynamicMetaObject BindSetMember(System.Dynamic.Se var parameters = new System.Linq.Expressions.Expression[] { System.Linq.Expressions.Expression.Constant(binder.Name), - value.Expression, + System.Linq.Expressions.Expression.Convert(value.Expression, typeof(object)), }; var callMethod = CallMethod(setValueMethod, parameters); diff --git a/Dapper/SqlMapper.DontMap.cs b/Dapper/SqlMapper.DontMap.cs index a97c5811e..765c7b937 100644 --- a/Dapper/SqlMapper.DontMap.cs +++ b/Dapper/SqlMapper.DontMap.cs @@ -5,6 +5,6 @@ public static partial class SqlMapper /// /// Dummy type for excluding from multi-map /// - private class DontMap { /* hiding constructor */ } + private sealed class DontMap { /* hiding constructor */ } } } diff --git a/Dapper/SqlMapper.GridReader.Async.cs b/Dapper/SqlMapper.GridReader.Async.cs index 5b5b9b73d..a32d53124 100644 --- a/Dapper/SqlMapper.GridReader.Async.cs +++ b/Dapper/SqlMapper.GridReader.Async.cs @@ -10,9 +10,7 @@ namespace Dapper public static partial class SqlMapper { public partial class GridReader -#if NET5_0_OR_GREATER : IAsyncDisposable -#endif { /// /// Read the next grid of results, returned as a dynamic object @@ -161,11 +159,8 @@ protected async Task OnAfterGridAsync(int index) #endif reader = null!; onCompleted?.Invoke(state); -#if NET5_0_OR_GREATER + await DisposeAsync(); -#else - Dispose(); -#endif } } @@ -247,7 +242,6 @@ private async Task> ReadBufferedAsync(int index, Func /// Read the next grid of results. /// @@ -283,19 +277,29 @@ private async IAsyncEnumerable ReadUnbufferedAsync(int index, Func /// Dispose the grid, closing and disposing both the underlying reader and command. /// +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - for netfx version public async ValueTask DisposeAsync() +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { if (reader is not null) { if (!reader.IsClosed) Command?.Cancel(); +#if NET5_0_OR_GREATER await reader.DisposeAsync(); +#else + reader.Dispose(); +#endif reader = null!; } if (Command is not null) { if (Command is DbCommand typed) { +#if NET5_0_OR_GREATER await typed.DisposeAsync(); +#else + typed.Dispose(); +#endif } else { @@ -305,7 +309,6 @@ public async ValueTask DisposeAsync() } GC.SuppressFinalize(this); } -#endif } } } diff --git a/Dapper/SqlMapper.Link.cs b/Dapper/SqlMapper.Link.cs index 92b434881..710714412 100644 --- a/Dapper/SqlMapper.Link.cs +++ b/Dapper/SqlMapper.Link.cs @@ -12,7 +12,7 @@ public static partial class SqlMapper /// /// The type to cache. /// The value type of the cache. - internal class Link where TKey : class + internal sealed class Link where TKey : class { public static void Clear(ref Link? head) => Interlocked.Exchange(ref head, null); public static bool TryGet(Link? link, TKey key, [NotNullWhen(true)] out TValue? value) diff --git a/Dapper/SqlMapper.Settings.cs b/Dapper/SqlMapper.Settings.cs index 343906b74..c19835c11 100644 --- a/Dapper/SqlMapper.Settings.cs +++ b/Dapper/SqlMapper.Settings.cs @@ -11,9 +11,10 @@ public static partial class SqlMapper /// public static class Settings { - // disable single result by default; prevents errors AFTER the select being detected properly - private const CommandBehavior DefaultAllowedCommandBehaviors = ~CommandBehavior.SingleResult; + // disable single row/result by default; prevents errors AFTER the select being detected properly + private const CommandBehavior DefaultAllowedCommandBehaviors = ~(CommandBehavior.SingleResult | CommandBehavior.SingleRow); internal static CommandBehavior AllowedCommandBehaviors { get; private set; } = DefaultAllowedCommandBehaviors; + private static void SetAllowedCommandBehaviors(CommandBehavior behavior, bool enabled) { if (enabled) AllowedCommandBehaviors |= behavior; @@ -64,7 +65,7 @@ static Settings() public static void SetDefaults() { CommandTimeout = null; - ApplyNullValues = PadListExpansions = UseIncrementalPseudoPositionalParameterNames = false; + ApplyNullValues = PadListExpansions = UseIncrementalPseudoPositionalParameterNames = PreferTypeHandlersForEnums = false; AllowedCommandBehaviors = DefaultAllowedCommandBehaviors; FetchSize = InListStringSplitCount = -1; } @@ -128,6 +129,13 @@ public static long FetchSize /// public static bool SupportLegacyParameterTokens { get; set; } = true; + /// + /// When true, Dapper checks for a registered TypeHandler for enum types before + /// falling back to the default behavior of sending enums as their underlying integer type. + /// This enables custom enum serialization (e.g. storing enums as strings), while preserving existing behavior. + /// + public static bool PreferTypeHandlersForEnums { get; set; } + private static long s_FetchSize = -1; } } diff --git a/Dapper/SqlMapper.TypeDeserializerCache.cs b/Dapper/SqlMapper.TypeDeserializerCache.cs index 2c2f646db..3a34cd292 100644 --- a/Dapper/SqlMapper.TypeDeserializerCache.cs +++ b/Dapper/SqlMapper.TypeDeserializerCache.cs @@ -8,7 +8,7 @@ namespace Dapper { public static partial class SqlMapper { - private class TypeDeserializerCache + private sealed class TypeDeserializerCache { private TypeDeserializerCache(Type type) { diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs index f0c909fce..2fa0e72b7 100644 --- a/Dapper/SqlMapper.cs +++ b/Dapper/SqlMapper.cs @@ -29,7 +29,7 @@ namespace Dapper /// public static partial class SqlMapper { - private class PropertyInfoByNameComparer : IComparer + private sealed class PropertyInfoByNameComparer : IComparer { public int Compare(PropertyInfo? x, PropertyInfo? y) => string.CompareOrdinal(x?.Name, y?.Name); } @@ -192,6 +192,7 @@ public TypeMapEntry(DbType dbType, TypeMapEntryFlags flags) public bool Equals(TypeMapEntry other) => other.DbType == DbType && other.Flags == Flags; public static readonly TypeMapEntry DoNotSet = new((DbType)(-2), TypeMapEntryFlags.None), + DoNotSetFieldValue = new((DbType)(-2), TypeMapEntryFlags.UseGetFieldValue), DecimalFieldValue = new(DbType.Decimal, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue); public static implicit operator TypeMapEntry(DbType dbType) @@ -200,7 +201,11 @@ public static implicit operator TypeMapEntry(DbType dbType) static SqlMapper() { - typeMap = new Dictionary(41) + typeMap = new Dictionary(41 +#if NET6_0_OR_GREATER && DATEONLY + + 4 // {Date|Time}Only[?] +#endif + ) { [typeof(byte)] = DbType.Byte, [typeof(sbyte)] = DbType.SByte, @@ -243,6 +248,12 @@ static SqlMapper() [typeof(SqlDecimal?)] = TypeMapEntry.DecimalFieldValue, [typeof(SqlMoney)] = TypeMapEntry.DecimalFieldValue, [typeof(SqlMoney?)] = TypeMapEntry.DecimalFieldValue, +#if NET6_0_OR_GREATER && DATEONLY + [typeof(DateOnly)] = TypeMapEntry.DoNotSetFieldValue, + [typeof(TimeOnly)] = TypeMapEntry.DoNotSetFieldValue, + [typeof(DateOnly?)] = TypeMapEntry.DoNotSetFieldValue, + [typeof(TimeOnly?)] = TypeMapEntry.DoNotSetFieldValue, +#endif }; ResetTypeHandlers(false); } @@ -255,11 +266,14 @@ static SqlMapper() [MemberNotNull(nameof(typeHandlers))] private static void ResetTypeHandlers(bool clone) { - typeHandlers = new Dictionary(); - AddTypeHandlerImpl(typeof(DataTable), new DataTableHandler(), clone); - AddTypeHandlerImpl(typeof(XmlDocument), new XmlDocumentHandler(), clone); - AddTypeHandlerImpl(typeof(XDocument), new XDocumentHandler(), clone); - AddTypeHandlerImpl(typeof(XElement), new XElementHandler(), clone); + lock (typeHandlersSyncLock) + { + typeHandlers = []; + AddTypeHandlerCore(typeof(DataTable), new DataTableHandler(), clone); + AddTypeHandlerCore(typeof(XmlDocument), new XmlDocumentHandler(), clone); + AddTypeHandlerCore(typeof(XDocument), new XDocumentHandler(), clone); + AddTypeHandlerCore(typeof(XElement), new XElementHandler(), clone); + } } /// @@ -328,7 +342,7 @@ public static void RemoveTypeMap(Type type) /// /// The type to handle. /// The handler to process the . - public static void AddTypeHandler(Type type, ITypeHandler handler) => AddTypeHandlerImpl(type, handler, true); + public static void AddTypeHandler(Type type, ITypeHandler handler) => AddTypeHandlerCore(type, handler, true); /// /// Determine if the specified type will be processed by a custom handler. /// @@ -342,7 +356,16 @@ public static void RemoveTypeMap(Type type) /// The type to handle. /// The handler to process the . /// Whether to clone the current type handler map. + [Obsolete("Please use " + nameof(AddTypeHandler), error: true)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public static void AddTypeHandlerImpl(Type type, ITypeHandler? handler, bool clone) + { + // this method was accidentally made public; we'll mark it as illegal, but + // preserve existing usage in compiled code; sorry about this! + AddTypeHandlerCore(type, handler, true); // do not allow suppress clone + } + + private static void AddTypeHandlerCore(Type type, ITypeHandler? handler, bool clone) { if (type is null) throw new ArgumentNullException(nameof(type)); @@ -362,29 +385,34 @@ public static void AddTypeHandlerImpl(Type type, ITypeHandler? handler, bool clo } } - var snapshot = typeHandlers; - if (snapshot.TryGetValue(type, out var oldValue) && handler == oldValue) return; // nothing to do + // synchronize between callers mutating type-handlers; note that regular query + // code may still be accessing the field, so we still use snapshot/mutate/swap; + // the synchronize is just to prevent lost writes + lock (typeHandlersSyncLock) + { + if (typeHandlers.TryGetValue(type, out var oldValue) && handler == oldValue) return; // nothing to do - var newCopy = clone ? new Dictionary(snapshot) : snapshot; + var newCopy = clone ? new Dictionary(typeHandlers) : typeHandlers; #pragma warning disable 618 - typeof(TypeHandlerCache<>).MakeGenericType(type).GetMethod(nameof(TypeHandlerCache.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, new object?[] { handler }); - if (secondary is not null) - { - typeof(TypeHandlerCache<>).MakeGenericType(secondary).GetMethod(nameof(TypeHandlerCache.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, new object?[] { handler }); - } + typeof(TypeHandlerCache<>).MakeGenericType(type).GetMethod(nameof(TypeHandlerCache.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, [handler]); + if (secondary is not null) + { + typeof(TypeHandlerCache<>).MakeGenericType(secondary).GetMethod(nameof(TypeHandlerCache.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, [handler]); + } #pragma warning restore 618 - if (handler is null) - { - newCopy.Remove(type); - if (secondary is not null) newCopy.Remove(secondary); - } - else - { - newCopy[type] = handler; - if (secondary is not null) newCopy[secondary] = handler; + if (handler is null) + { + newCopy.Remove(type); + if (secondary is not null) newCopy.Remove(secondary); + } + else + { + newCopy[type] = handler; + if (secondary is not null) newCopy[secondary] = handler; + } + typeHandlers = newCopy; } - typeHandlers = newCopy; } /// @@ -392,9 +420,10 @@ public static void AddTypeHandlerImpl(Type type, ITypeHandler? handler, bool clo /// /// The type to handle. /// The handler for the type . - public static void AddTypeHandler(TypeHandler handler) => AddTypeHandlerImpl(typeof(T), handler, true); + public static void AddTypeHandler(TypeHandler handler) => AddTypeHandlerCore(typeof(T), handler, true); private static Dictionary typeHandlers; + private static readonly object typeHandlersSyncLock = new(); internal const string LinqBinary = "System.Data.Linq.Binary"; @@ -436,6 +465,10 @@ public static void SetDbType(IDataParameter parameter, object value) if (nullUnderlyingType is not null) type = nullUnderlyingType; if (type.IsEnum && !typeMap.ContainsKey(type)) { + if (Settings.PreferTypeHandlersForEnums && typeHandlers.TryGetValue(type, out handler)) + { + return DbType.Object; + } type = Enum.GetUnderlyingType(type); } if (typeMap.TryGetValue(type, out var mapEntry)) @@ -468,7 +501,7 @@ public static void SetDbType(IDataParameter parameter, object value) { handler = (ITypeHandler)Activator.CreateInstance( typeof(SqlDataRecordHandler<>).MakeGenericType(argTypes))!; - AddTypeHandlerImpl(type, handler, true); + AddTypeHandlerCore(type, handler, true); return DbType.Object; } catch @@ -1137,7 +1170,7 @@ private static GridReader QueryMultipleImpl(this IDbConnection cnn, ref CommandD if (!reader.IsClosed) { try { cmd?.Cancel(); } - catch { /* don't spoil the existing exception */ } + catch { /* don't spoil any existing exception */ } } reader.Dispose(); } @@ -1218,7 +1251,7 @@ private static IEnumerable QueryImpl(this IDbConnection cnn, CommandDefini if (!reader.IsClosed) { try { cmd?.Cancel(); } - catch { /* don't spoil the existing exception */ } + catch { /* don't spoil any existing exception */ } } reader.Dispose(); } @@ -1238,7 +1271,7 @@ internal enum Row SingleOrDefault = 3 } - private static readonly int[] ErrTwoRows = new int[2], ErrZeroRows = Array.Empty(); + private static readonly int[] ErrTwoRows = new int[2], ErrZeroRows = []; private static void ThrowMultipleRows(Row row) { _ = row switch @@ -1310,7 +1343,7 @@ private static T QueryRowImpl(IDbConnection cnn, Row row, ref CommandDefiniti if (!reader.IsClosed) { try { cmd?.Cancel(); } - catch { /* don't spoil the existing exception */ } + catch { /* don't spoil any existing exception */ } } reader.Dispose(); } @@ -1647,7 +1680,7 @@ private static IEnumerable MultiMapImpl(this IDbConnection? cn var deserializers = GenerateDeserializers(identity, splitOn, reader); deserializer = cinfo.Deserializer = new DeserializerState(hash, deserializers[0]); otherDeserializers = cinfo.OtherDeserializers = deserializers.Skip(1).ToArray(); - SetQueryCache(identity, cinfo); + if (command.AddToCache) SetQueryCache(identity, cinfo); } Func mapIt = GenerateMapper(types.Length, deserializer.Func, otherDeserializers, map); @@ -2536,7 +2569,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true filterParams = !CompiledRegex.LegacyParameter.IsMatch(identity.Sql); } - var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, new[] { typeof(IDbCommand), typeof(object) }, type, true); + var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, [typeof(IDbCommand), typeof(object)], type, true); var il = dm.GetILGenerator(); @@ -2637,7 +2670,11 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true { il.Emit(OpCodes.Ldloc, typedParameterLocal); // stack is now [parameters] [typed-param] il.Emit(callOpCode, prop.GetGetMethod()!); // stack is [parameters] [custom] - if (!prop.PropertyType.IsValueType) + if (prop.PropertyType.IsValueType) + { + il.Emit(OpCodes.Box, prop.PropertyType); // stack is [parameters] [boxed-custom] + } + else { // throw if null var notNull = il.DefineLabel(); @@ -2649,7 +2686,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true } il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [custom] [command] il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [custom] [command] [name] - il.EmitCall(OpCodes.Callvirt, prop.PropertyType.GetMethod(nameof(ICustomQueryParameter.AddParameter))!, null); // stack is now [parameters] + il.EmitCall(OpCodes.Callvirt, typeof(ICustomQueryParameter).GetMethod(nameof(ICustomQueryParameter.AddParameter))!, null); // stack is now [parameters] continue; } #pragma warning disable 618 @@ -2722,7 +2759,12 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true if ((nullType ?? propType).IsEnum) { - if (nullType is not null) + if (handler is not null) + { + // TypeHandler registered - box as the enum type, handler does conversion + checkForNull = nullType is not null; + } + else if (nullType is not null) { // Nullable; we want to box as the underlying type; that's just *hard*; for // simplicity, box as Nullable and call SanitizeParameterValue @@ -2907,7 +2949,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true { if (locals is null) { - locals = new Dictionary(); + locals = []; local = null; } else @@ -2944,14 +2986,14 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true { typeof(bool), typeof(sbyte), typeof(byte), typeof(ushort), typeof(short), typeof(uint), typeof(int), typeof(ulong), typeof(long), typeof(float), typeof(double), typeof(decimal) - }.ToDictionary(x => Type.GetTypeCode(x), x => x.GetPublicInstanceMethod(nameof(object.ToString), new[] { typeof(IFormatProvider) })!); + }.ToDictionary(x => Type.GetTypeCode(x), x => x.GetPublicInstanceMethod(nameof(object.ToString), [typeof(IFormatProvider)])!); private static MethodInfo? GetToString(TypeCode typeCode) { return toStrings.TryGetValue(typeCode, out MethodInfo? method) ? method : null; } - private static readonly MethodInfo StringReplace = typeof(string).GetPublicInstanceMethod(nameof(string.Replace), new Type[] { typeof(string), typeof(string) })!, + private static readonly MethodInfo StringReplace = typeof(string).GetPublicInstanceMethod(nameof(string.Replace), [typeof(string), typeof(string)])!, InvariantCulture = typeof(CultureInfo).GetProperty(nameof(CultureInfo.InvariantCulture), BindingFlags.Public | BindingFlags.Static)!.GetGetMethod()!; private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition command, Action? paramReader) @@ -3068,7 +3110,16 @@ private static Func GetSimpleValueDeserializer(Type type, #pragma warning restore 618 if (effectiveType.IsEnum) - { // assume the value is returned as the correct type (int/byte/etc), but box back to the typed enum + { + if (Settings.PreferTypeHandlersForEnums && typeHandlers.TryGetValue(type, out var enumHandler)) + { + return r => + { + var val = r.GetValue(index); + return val is DBNull ? null! : enumHandler.Parse(type, val)!; + }; + } + // assume the value is returned as the correct type (int/byte/etc), but box back to the typed enum return r => { var val = r.GetValue(index); @@ -3115,7 +3166,7 @@ static Func ReadViaGetFieldValueFactory(Type type, int ind return factory(index); } // cache of ReadViaGetFieldValueFactory for per-value T - static readonly Hashtable s_ReadViaGetFieldValueCache = new(); + static readonly Hashtable s_ReadViaGetFieldValueCache = []; static Func UnderlyingReadViaGetFieldValueFactory(int index) => reader => reader.IsDBNull(index) ? null! : reader.GetFieldValue(index)!; @@ -3131,6 +3182,10 @@ private static T Parse(object? value) type = Nullable.GetUnderlyingType(type) ?? type; if (type.IsEnum) { + if (Settings.PreferTypeHandlersForEnums && typeHandlers.TryGetValue(type, out ITypeHandler? enumHandler)) + { + return (T)enumHandler.Parse(type, value)!; + } if (value is float || value is double || value is decimal) { value = Convert.ChangeType(value, Enum.GetUnderlyingType(type), CultureInfo.InvariantCulture); @@ -3145,14 +3200,14 @@ private static T Parse(object? value) } private static readonly MethodInfo - enumParse = typeof(Enum).GetMethod(nameof(Enum.Parse), new Type[] { typeof(Type), typeof(string), typeof(bool) })!, + enumParse = typeof(Enum).GetMethod(nameof(Enum.Parse), [typeof(Type), typeof(string), typeof(bool)])!, getItem = typeof(DbDataReader).GetProperties(BindingFlags.Instance | BindingFlags.Public) .Where(p => p.GetIndexParameters().Length > 0 && p.GetIndexParameters()[0].ParameterType == typeof(int)) .Select(p => p.GetGetMethod()).First()!, getFieldValueT = typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetFieldValue), - BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(int) }, null)!, + BindingFlags.Instance | BindingFlags.Public, null, [typeof(int)], null)!, isDbNull = typeof(DbDataReader).GetMethod(nameof(DbDataReader.IsDBNull), - BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(int) }, null)!; + BindingFlags.Instance | BindingFlags.Public, null, [typeof(int)], null)!; /// /// Gets type-map for the given type @@ -3189,7 +3244,7 @@ public static ITypeMap GetTypeMap(Type type) } // use Hashtable to get free lockless reading - private static readonly Hashtable _typeMaps = new(); + private static readonly Hashtable _typeMaps = []; /// /// Set custom mapping for type deserializers @@ -3261,7 +3316,7 @@ public static Func GetTypeDeserializer( private static LocalBuilder GetTempLocal(ILGenerator il, ref Dictionary? locals, Type type, bool initAndLoad) { if (type is null) throw new ArgumentNullException(nameof(type)); - locals ??= new Dictionary(); + locals ??= []; if (!locals.TryGetValue(type, out LocalBuilder? found)) { found = il.DeclareLocal(type); @@ -3292,7 +3347,7 @@ private static Func GetTypeDeserializerImpl( } var returnType = type.IsValueType ? typeof(object) : type; - var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, new[] { typeof(DbDataReader) }, type, true); + var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, [typeof(DbDataReader)], type, true); var il = dm.GetILGenerator(); if (IsValueTuple(type)) @@ -3401,7 +3456,7 @@ private static void GenerateValueTupleDeserializer(Type valueTupleType, DbDataRe if (nullableUnderlyingType is not null) { - var nullableTupleConstructor = valueTupleType.GetConstructor(new[] { nullableUnderlyingType }); + var nullableTupleConstructor = valueTupleType.GetConstructor([nullableUnderlyingType]); il.Emit(OpCodes.Newobj, nullableTupleConstructor!); } @@ -3501,9 +3556,9 @@ private static void GenerateDeserializerFromMap(Type type, DbDataReader reader, il.Emit(OpCodes.Ldloc, returnValueLocal); // [target] } - var members = (specializedConstructor is not null - ? names.Select(n => typeMap.GetConstructorParameter(specializedConstructor, n)) - : names.Select(n => typeMap.GetMember(n))).ToList(); + var members = Array.ConvertAll(names, specializedConstructor is not null + ? n => typeMap.GetConstructorParameter(specializedConstructor, n) + : n => typeMap.GetMember(n)); // stack is now [target] bool first = true; @@ -3666,7 +3721,7 @@ private static void LoadReaderValueViaGetFieldValue(ILGenerator il, int index, T if (underlyingType != memberType) { // Nullable; wrap it - il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { underlyingType })!); // stack is now [...][T?] + il.Emit(OpCodes.Newobj, memberType.GetConstructor([underlyingType])!); // stack is now [...][T?] } } @@ -3709,33 +3764,42 @@ private static void LoadReaderValueOrBranchToDBNullLabel(ILGenerator il, int ind if (unboxType.IsEnum) { - Type numericType = Enum.GetUnderlyingType(unboxType); - if (colType == typeof(string)) + if (Settings.PreferTypeHandlersForEnums && typeHandlers.ContainsKey(unboxType)) { - stringEnumLocal ??= il.DeclareLocal(typeof(string)); - il.Emit(OpCodes.Castclass, typeof(string)); // stack is now [...][string] - il.Emit(OpCodes.Stloc, stringEnumLocal); // stack is now [...] - il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [...][enum-type-token] - il.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!, null);// stack is now [...][enum-type] - il.Emit(OpCodes.Ldloc, stringEnumLocal); // stack is now [...][enum-type][string] - il.Emit(OpCodes.Ldc_I4_1); // stack is now [...][enum-type][string][true] - il.EmitCall(OpCodes.Call, enumParse, null); // stack is now [...][enum-as-object] - il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [...][typed-value] +#pragma warning disable 618 + il.EmitCall(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(unboxType).GetMethod(nameof(TypeHandlerCache.Parse))!, null); // stack is now [...][typed-value] +#pragma warning restore 618 } else { - FlexibleConvertBoxedFromHeadOfStack(il, colType, unboxType, numericType); + Type numericType = Enum.GetUnderlyingType(unboxType); + if (colType == typeof(string)) + { + stringEnumLocal ??= il.DeclareLocal(typeof(string)); + il.Emit(OpCodes.Castclass, typeof(string)); // stack is now [...][string] + il.Emit(OpCodes.Stloc, stringEnumLocal); // stack is now [...] + il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [...][enum-type-token] + il.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!, null);// stack is now [...][enum-type] + il.Emit(OpCodes.Ldloc, stringEnumLocal); // stack is now [...][enum-type][string] + il.Emit(OpCodes.Ldc_I4_1); // stack is now [...][enum-type][string][true] + il.EmitCall(OpCodes.Call, enumParse, null); // stack is now [...][enum-as-object] + il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [...][typed-value] + } + else + { + FlexibleConvertBoxedFromHeadOfStack(il, colType, unboxType, numericType); + } } if (nullUnderlyingType is not null) { - il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { nullUnderlyingType })!); // stack is now [...][typed-value] + il.Emit(OpCodes.Newobj, memberType.GetConstructor([nullUnderlyingType])!); // stack is now [...][typed-value] } } else if (memberType.FullName == LinqBinary) { il.Emit(OpCodes.Unbox_Any, typeof(byte[])); // stack is now [...][byte-array] - il.Emit(OpCodes.Newobj, memberType.GetConstructor(new Type[] { typeof(byte[]) })!);// stack is now [...][binary] + il.Emit(OpCodes.Newobj, memberType.GetConstructor([typeof(byte[])])!);// stack is now [...][binary] } else { @@ -3760,7 +3824,7 @@ private static void LoadReaderValueOrBranchToDBNullLabel(ILGenerator il, int ind FlexibleConvertBoxedFromHeadOfStack(il, colType, nullUnderlyingType ?? unboxType, null); if (nullUnderlyingType is not null) { - il.Emit(OpCodes.Newobj, unboxType.GetConstructor(new[] { nullUnderlyingType })!); // stack is now [...][typed-value] + il.Emit(OpCodes.Newobj, unboxType.GetConstructor([nullUnderlyingType])!); // stack is now [...][typed-value] } } } @@ -3844,7 +3908,7 @@ private static void FlexibleConvertBoxedFromHeadOfStack(ILGenerator il, Type fro il.Emit(OpCodes.Ldtoken, via ?? to); // stack is now [target][target][value][member-type-token] il.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!, null); // stack is now [target][target][value][member-type] il.EmitCall(OpCodes.Call, InvariantCulture, null); // stack is now [target][target][value][member-type][culture] - il.EmitCall(OpCodes.Call, typeof(Convert).GetMethod(nameof(Convert.ChangeType), new Type[] { typeof(object), typeof(Type), typeof(IFormatProvider) })!, null); // stack is now [target][target][boxed-member-type-value] + il.EmitCall(OpCodes.Call, typeof(Convert).GetMethod(nameof(Convert.ChangeType), [typeof(object), typeof(Type), typeof(IFormatProvider)])!, null); // stack is now [target][target][boxed-member-type-value] il.Emit(OpCodes.Unbox_Any, to); // stack is now [target][target][typed-value] } } @@ -3905,13 +3969,22 @@ public static void ThrowDataException(Exception ex, int index, IDataReader reade } try { - if (value is null || value is DBNull) + if (value is null && ex is InvalidCastException) + { + formattedValue = "n/a - " + ex.Message; // provide some context + } + else if (value is null || value is DBNull) { formattedValue = ""; } else { - formattedValue = Convert.ToString(value) + " - " + Type.GetTypeCode(value.GetType()); + formattedValue = Convert.ToString(value) + " - " + Identify(value.GetType()); + } + static string Identify(Type type) + { + var tc = Type.GetTypeCode(type); + return tc == TypeCode.Object ? type.Name : tc.ToString(); } } catch (Exception valEx) diff --git a/Dapper/WrappedReader.cs b/Dapper/WrappedReader.cs index 42fd6da93..04321f976 100644 --- a/Dapper/WrappedReader.cs +++ b/Dapper/WrappedReader.cs @@ -129,7 +129,6 @@ protected override void Dispose(bool disposing) { if (disposing) { - _reader.Close(); _reader.Dispose(); _reader = DisposedReader.Instance; // all future ops are no-ops _cmd?.Dispose(); @@ -245,7 +244,6 @@ protected override void Dispose(bool disposing) { if (disposing) { - _reader.Close(); _reader.Dispose(); _reader = DisposedReader.Instance; // all future ops are no-ops } diff --git a/Directory.Build.props b/Directory.Build.props index 6a4b94e1c..227e4eed5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,17 +14,18 @@ git https://github.com/DapperLib/Dapper false - $(NOWARN);IDE0056;IDE0057;IDE0079 + $(NOWARN);IDE0056;IDE0057;IDE0079;DX1001 true embedded en-US false true true - 12 + 13 false true readme.md + true diff --git a/Directory.Packages.props b/Directory.Packages.props index 4c55df6f4..252f417b9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,46 +1,58 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NonCLA.md b/NonCLA.md new file mode 100644 index 000000000..f5c9414ef --- /dev/null +++ b/NonCLA.md @@ -0,0 +1,35 @@ +# Dapper - the "Non CLA" CLA + +IANAL. YANAL (I hope). Let's keep this simple; if you want to contribute to Dapper, great! Let's just check a few things for the record. + +By accepting this agreement, you're saying: + +## You're allowed to contribute this code + +The code needs to be yours (without being owned by some employer, etc), or contributed with the owner's knowledge and permission, or +in accordance with a licence that clearly allows the code to be reused in line with this project's licence (http://www.apache.org/licenses/LICENSE-2.0), +and in that last case: a cross-reference back to the origin would be nice. + +(for the pedants: "licence" and "license" to be used interchangeably here; language is fun) + +## It isn't "your code" any more + +Contributed code belongs to the Dapper project, not you the contributor. That means +Dapper can use it, not use it, remove it, or +edit it in any way - *even changing the spacing and variable names*. I know: shocking. + +## No backsies + +Contributing code to Dapper is permanent; you can't later demand that we remove your code because... well, anything. Even if one +of the maintainers wears socks that look *really ugly*. + +## For gratis + +Contributing code to Dapper gets you the bugfix or feature or whatever that you want; you have our thanks and appreciation, but unless we've +agreed something separately: that's it. No turning up unannounced at the tri-annual BBQ, or demanding... again, anything. + +--- + +That's it. Basically "don't make our life harder". + +Thanks! diff --git a/Readme.md b/Readme.md index 8c8be6bd7..d7bc4afaa 100644 --- a/Readme.md +++ b/Readme.md @@ -38,8 +38,11 @@ Sponsors Dapper was originally developed for and by Stack Overflow, but is F/OSS. Sponsorship is welcome and invited - see the sponsor link at the top of the page. A huge thanks to everyone (individuals or organisations) who have sponsored Dapper, but a massive thanks in particular to: +- [Dapper Plus](https://dapper-plus.net/) is a major sponsor and is proud to contribute to the development of Dapper ([read more](https://dapperlib.github.io/Dapper/dapperplus)) - [AWS](https://github.com/aws) who sponsored Dapper from Oct 2023 via the [.NET on AWS Open Source Software Fund](https://github.com/aws/dotnet-foss) +Dapper Plus logo + Features -------- Dapper is a [NuGet library](https://www.nuget.org/packages/Dapper) that you can add in to your project that will enhance your ADO.NET connections via @@ -446,7 +449,7 @@ using (var reader = connection.ExecuteReader("select * from Shapes")) } ``` -User Defined Variables in MySQL +User Defined Variables in MySQL/MariaDB --------------------- In order to use Non-parameter SQL variables with MySql Connector, you have to add the following option to your connection string: @@ -462,7 +465,7 @@ Dapper's simplicity means that many features that ORMs ship with are stripped ou Will Dapper work with my DB provider? --------------------- -Dapper has no DB specific implementation details, it works across all .NET ADO providers including [SQLite](https://www.sqlite.org/), SQL CE, Firebird, Oracle, MySQL, PostgreSQL and SQL Server. +Dapper has no DB specific implementation details, it works across all .NET ADO providers including [SQLite](https://www.sqlite.org/), SQL CE, Firebird, Oracle, MariaDB, MySQL, PostgreSQL and SQL Server. Do you have a comprehensive list of examples? --------------------- diff --git a/appveyor.yml b/appveyor.yml index 08c5eb159..5ec88faaf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,13 +6,15 @@ skip_commits: files: - '**/*.md' -# install: -# - choco install dotnet-sdk --version 8.0.100 +install: +- ps: | + Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -UseBasicParsing -OutFile "$env:temp\dotnet-install.ps1" + & $env:temp\dotnet-install.ps1 -Architecture x64 -Version '10.0.102' -InstallDir "$env:ProgramFiles\dotnet" environment: Appveyor: true # Postgres - POSTGRES_PATH: C:\Program Files\PostgreSQL\9.6 + POSTGRES_PATH: C:\Program Files\PostgreSQL\16 PGUSER: postgres PGPASSWORD: Password12! POSTGRES_ENV_POSTGRES_USER: postgres @@ -30,14 +32,11 @@ environment: PostgesConnectionString: Server=localhost;Port=5432;User Id=postgres;Password=Password12!;Database=test SqlServerConnectionString: Server=(local)\SQL2019;Database=tempdb;User ID=sa;Password=Password12! -services: - # - mysql - - postgresql - init: - git config --global core.autocrlf input - SET PATH=%POSTGRES_PATH%\bin;%MYSQL_PATH%\bin;%PATH% - net start MSSQL$SQL2019 + - net start postgresql-x64-16 - ps: Start-Service MySQL80 nuget: diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.HandCoded.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.HandCoded.cs index 05013e465..3fcfc6d3d 100644 --- a/benchmarks/Dapper.Tests.Performance/Benchmarks.HandCoded.cs +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.HandCoded.cs @@ -2,7 +2,7 @@ using System; using System.ComponentModel; using System.Data; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; namespace Dapper.Tests.Performance { diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.RepoDB.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.RepoDB.cs index 10694ea79..dd762afcf 100644 --- a/benchmarks/Dapper.Tests.Performance/Benchmarks.RepoDB.cs +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.RepoDB.cs @@ -20,11 +20,11 @@ public void Setup() // We need this since benchmarks using System.Data.SqlClient var dbSetting = new SqlServerDbSetting(); DbSettingMapper - .Add(dbSetting, true); + .Add(dbSetting, true); DbHelperMapper - .Add(new SqlServerDbHelper(), true); + .Add(new SqlServerDbHelper(), true); StatementBuilderMapper - .Add(new SqlServerStatementBuilder(dbSetting), true); + .Add(new SqlServerStatementBuilder(dbSetting), true); ClassMapper.Add("Posts"); } @@ -54,7 +54,7 @@ public Post QueryDynamic() public Post QueryField() { Step(); - return _connection.Query(new QueryField[] { new(nameof(Post.Id), i) }).First(); + return _connection.Query([new(nameof(Post.Id), i)]).First(); } [Benchmark(Description = "ExecuteQuery")] diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.cs index aa5effb53..8a92b5e23 100644 --- a/benchmarks/Dapper.Tests.Performance/Benchmarks.cs +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.cs @@ -1,7 +1,7 @@ using BenchmarkDotNet.Attributes; using System; using System.Configuration; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; namespace Dapper.Tests.Performance { diff --git a/benchmarks/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj b/benchmarks/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj index 52396c03e..51c28957a 100644 --- a/benchmarks/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj +++ b/benchmarks/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj @@ -3,7 +3,7 @@ Dapper.Tests.Performance Dapper Core Performance Suite Exe - net462;net8.0 + net472;net10.0 false $(NoWarn);IDE0063;IDE0034;IDE0059;IDE0060 @@ -14,40 +14,56 @@ - - + - - + + + + + + + + + + + + + + + + + + + + - + $(DefineConstants);NET4X - + - - + - + diff --git a/benchmarks/Dapper.Tests.Performance/LegacyTests.cs b/benchmarks/Dapper.Tests.Performance/LegacyTests.cs index dd9588b43..9bf4cdb46 100644 --- a/benchmarks/Dapper.Tests.Performance/LegacyTests.cs +++ b/benchmarks/Dapper.Tests.Performance/LegacyTests.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Data; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; using System.Diagnostics; using System.Linq; diff --git a/benchmarks/Dapper.Tests.Performance/Linq2DB/Linq2DBContext.cs b/benchmarks/Dapper.Tests.Performance/Linq2DB/Linq2DBContext.cs index 0922cd72c..43d491786 100644 --- a/benchmarks/Dapper.Tests.Performance/Linq2DB/Linq2DBContext.cs +++ b/benchmarks/Dapper.Tests.Performance/Linq2DB/Linq2DBContext.cs @@ -1,10 +1,9 @@ using LinqToDB; -using Microsoft.EntityFrameworkCore; namespace Dapper.Tests.Performance.Linq2Db { public class Linq2DBContext : LinqToDB.Data.DataConnection { - public ITable Posts => GetTable(); + public ITable Posts => this.GetTable(); } } diff --git a/benchmarks/Dapper.Tests.Performance/Massive/Massive.cs b/benchmarks/Dapper.Tests.Performance/Massive/Massive.cs index f452a95cb..f4c1c8192 100644 --- a/benchmarks/Dapper.Tests.Performance/Massive/Massive.cs +++ b/benchmarks/Dapper.Tests.Performance/Massive/Massive.cs @@ -322,7 +322,6 @@ public virtual DbCommand CreateUpdateCommand(object o, object key) var settings = (IDictionary)expando; var sbKeys = new StringBuilder(); const string stub = "UPDATE {0} SET {1} WHERE {2} = @{3}"; - var args = new List(); var result = CreateCommand(stub, null); int counter = 0; foreach (var item in settings) diff --git a/benchmarks/Dapper.Tests.Performance/PetaPoco/PetaPoco.cs b/benchmarks/Dapper.Tests.Performance/PetaPoco/PetaPoco.cs index add3df772..2ffa3887c 100644 --- a/benchmarks/Dapper.Tests.Performance/PetaPoco/PetaPoco.cs +++ b/benchmarks/Dapper.Tests.Performance/PetaPoco/PetaPoco.cs @@ -1020,10 +1020,9 @@ public string LastCommand { get { - var sb = new StringBuilder(); if (_lastSql == null) return ""; - sb.Append(_lastSql); + var sb = new StringBuilder(_lastSql); if (_lastArgs != null) { sb.Append("\r\n\r\n"); @@ -1038,14 +1037,14 @@ public string LastCommand public static IMapper Mapper { get; set; } - internal class PocoColumn + internal sealed class PocoColumn { public string ColumnName; public PropertyInfo PropertyInfo; public bool ResultColumn; } - internal class PocoData + internal sealed class PocoData { public static PocoData ForType(Type t) { @@ -1272,7 +1271,7 @@ public Func GetFactory(string key, bool ForceDateTimesToUtc, // ShareableConnection represents either a shared connection used by a transaction, // or a one-off connection if not in a transaction. // Non-shared connections are disposed - private class ShareableConnection : IDisposable + private sealed class ShareableConnection : IDisposable { public ShareableConnection(Database db) { diff --git a/benchmarks/Dapper.Tests.Performance/Program.cs b/benchmarks/Dapper.Tests.Performance/Program.cs index 1e102cf87..4ffe7de9a 100644 --- a/benchmarks/Dapper.Tests.Performance/Program.cs +++ b/benchmarks/Dapper.Tests.Performance/Program.cs @@ -1,6 +1,6 @@ using BenchmarkDotNet.Running; using System; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; using System.Linq; using static System.Console; diff --git a/benchmarks/Dapper.Tests.Performance/SqlDataReaderHelper.cs b/benchmarks/Dapper.Tests.Performance/SqlDataReaderHelper.cs index d6d286e96..f7c215cda 100644 --- a/benchmarks/Dapper.Tests.Performance/SqlDataReaderHelper.cs +++ b/benchmarks/Dapper.Tests.Performance/SqlDataReaderHelper.cs @@ -1,5 +1,5 @@ using System; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; using System.Runtime.CompilerServices; namespace Dapper.Tests.Performance diff --git a/build.ps1 b/build.ps1 index f406c745f..59193ff75 100644 --- a/build.ps1 +++ b/build.ps1 @@ -22,8 +22,8 @@ dotnet build ".\Build.csproj" -c Release /p:CI=true Write-Host "Done building." -ForegroundColor "Green" if ($RunTests) { - Write-Host "Running tests: Build.csproj traversal (all frameworks)" -ForegroundColor "Magenta" - dotnet test ".\Build.csproj" -c Release --no-build + Write-Host "Running tests: Build.csproj" -ForegroundColor "Magenta" + dotnet test ".\Build.csproj" -c Release --no-build -p:TestTfmsInParallel=false if ($LastExitCode -ne 0) { Write-Host "Error with tests, aborting build." -Foreground "Red" Exit 1 diff --git a/docs/dapper-sponsor.png b/docs/dapper-sponsor.png new file mode 100644 index 000000000..b9023052b Binary files /dev/null and b/docs/dapper-sponsor.png differ diff --git a/docs/dapperplus.md b/docs/dapperplus.md new file mode 100644 index 000000000..b15817721 --- /dev/null +++ b/docs/dapperplus.md @@ -0,0 +1,13 @@ +# Dapper and Dapper Plus + +Dapper is the micro-ORM developed initially by Stack Overflow and now maintained independently, that offers simple, high performance +access to the ADO.NET API. + +Dapper Plus is a separate tool by ZZZ Projects, which builds on the path set by Dapper, offering features like bulk operations, +and a range of [documentation for Dapper](https://www.learndapper.com/). + +From 2024, Dapper Plus is now a major sponsor of Dapper, helping to secure ongoing quality development and support of the Dapper platform. +This sponsorship does not impact the ownership, license, or any other particulars of how Dapper operates. The core Dapper libraries continue +to be freely available and fully open source. + +Dapper Plus logo \ No newline at end of file diff --git a/docs/dapperplus.png b/docs/dapperplus.png new file mode 100644 index 000000000..2911442c4 Binary files /dev/null and b/docs/dapperplus.png differ diff --git a/docs/docs.csproj b/docs/docs.csproj new file mode 100644 index 000000000..d68df5c4a --- /dev/null +++ b/docs/docs.csproj @@ -0,0 +1,6 @@ + + + + net8.0 + + diff --git a/docs/readme.md b/docs/readme.md index 19da484b7..e201fdcd8 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -14,4 +14,15 @@ var customers = connection.Query( But all the execute/single-row/scalar/async/etc functionality you would expect: is there as extension methods on your `DbConnection`. -See [GitHub](https://github.com/DapperLib/Dapper) for more information and examples. \ No newline at end of file +See [GitHub](https://github.com/DapperLib/Dapper) for more information and examples. + +Sponsors +-------- + +Dapper was originally developed for and by Stack Overflow, but is F/OSS. Sponsorship is welcome and invited - see the sponsor link at the top of the page. +A huge thanks to everyone (individuals or organisations) who have sponsored Dapper, but a massive thanks in particular to: + +- [Dapper Plus](https://dapper-plus.net/) is a major sponsor and is proud to contribute to the development of Dapper ([read more](https://dapperlib.github.io/Dapper/dapperplus)) +- [AWS](https://github.com/aws) who sponsored Dapper from Oct 2023 via the [.NET on AWS Open Source Software Fund](https://github.com/aws/dotnet-foss) + +[![Dapper Plus logo](https://raw.githubusercontent.com/DapperLib/Dapper/main/docs/dapper-sponsor.png)](https://dapper-plus.net/) diff --git a/global.json b/global.json index f3365c418..1e3e94a0b 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,6 @@ { "sdk": { - "version": "8.0.100" + "version": "10.0.102", + "rollForward": "latestMajor" } -} \ No newline at end of file +} diff --git a/signatures/version1/cla.json b/signatures/version1/cla.json new file mode 100644 index 000000000..18d5487f3 --- /dev/null +++ b/signatures/version1/cla.json @@ -0,0 +1,3 @@ +{ + "signedContributors": [] +} \ No newline at end of file diff --git a/tests/Dapper.Tests/AsyncTests.cs b/tests/Dapper.Tests/AsyncTests.cs index 7a9abf2d1..9c3ec4721 100644 --- a/tests/Dapper.Tests/AsyncTests.cs +++ b/tests/Dapper.Tests/AsyncTests.cs @@ -46,7 +46,6 @@ public async Task TestBasicStringUsageAsync() Assert.Equal(new[] { "abc", "def" }, arr); } -#if NET5_0_OR_GREATER [Fact] public async Task TestBasicStringUsageUnbufferedDynamicAsync() { @@ -158,7 +157,6 @@ public async Task TestBasicStringUsageViaGridReaderUnbufferedAsync_Cancellation( var arr = results.ToArray(); Assert.Equal(new[] { "abc", "def" }, arr); // don't expect the ghi because of cancellation } -#endif [Fact] public async Task TestBasicStringUsageQueryFirstAsync() @@ -669,6 +667,34 @@ public async Task TestSupportForDynamicParametersOutputExpressions_Query_Default Assert.Equal(42, result); } + [Fact] + public async Task TestSupportForDynamicParametersOutputExpressions_QueryFirst() + { + var bob = new Person { Name = "bob", PersonId = 1, Address = new Address { PersonId = 2 } }; + + var p = new DynamicParameters(bob); + p.Output(bob, b => b.PersonId); + p.Output(bob, b => b.Occupation); + p.Output(bob, b => b.NumberOfLegs); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); + + var result = (await connection.QueryFirstAsync(@" +SET @Occupation = 'grillmaster' +SET @PersonId = @PersonId + 1 +SET @NumberOfLegs = @NumberOfLegs - 1 +SET @AddressName = 'bobs burgers' +SET @AddressPersonId = @PersonId +select 42", p).ConfigureAwait(false)); + + Assert.Equal("grillmaster", bob.Occupation); + Assert.Equal(2, bob.PersonId); + Assert.Equal(1, bob.NumberOfLegs); + Assert.Equal("bobs burgers", bob.Address.Name); + Assert.Equal(2, bob.Address.PersonId); + Assert.Equal(42, result); + } + [Fact] public async Task TestSupportForDynamicParametersOutputExpressions_Query_BufferedAsync() { @@ -995,5 +1021,41 @@ public void AssertNoCacheWorksForQueryMultiple() Assert.Equal(123, c); Assert.Equal(456, d); } + + [Fact] + public async Task AssertNoCacheWorksForMultiMap() + { + const int a = 123, b = 456; + var cmdDef = new CommandDefinition("select @a as a, @b as b;", new + { + a, + b + }, commandType: CommandType.Text, flags: CommandFlags.NoCache | CommandFlags.Buffered); + + SqlMapper.PurgeQueryCache(); + var before = SqlMapper.GetCachedSQLCount(); + Assert.Equal(0, before); + + await MarsConnection.QueryAsync(cmdDef, splitOn: "b", map: (a, b) => (a, b)); + Assert.Equal(0, SqlMapper.GetCachedSQLCount()); + } + + [Fact] + public async Task AssertNoCacheWorksForQueryAsync() + { + const int a = 123, b = 456; + var cmdDef = new CommandDefinition("select @a as a, @b as b;", new + { + a, + b + }, commandType: CommandType.Text, flags: CommandFlags.NoCache | CommandFlags.Buffered); + + SqlMapper.PurgeQueryCache(); + var before = SqlMapper.GetCachedSQLCount(); + Assert.Equal(0, before); + + await MarsConnection.QueryAsync<(int, int)>(cmdDef); + Assert.Equal(0, SqlMapper.GetCachedSQLCount()); + } } } diff --git a/tests/Dapper.Tests/Dapper.Tests.csproj b/tests/Dapper.Tests/Dapper.Tests.csproj index 5863bc8ed..93b498618 100644 --- a/tests/Dapper.Tests/Dapper.Tests.csproj +++ b/tests/Dapper.Tests/Dapper.Tests.csproj @@ -2,34 +2,45 @@ Dapper.Tests Dapper Core Test Suite - net472;net6.0;net8.0 + net481;net8.0;net10.0 $(DefineConstants);MSSQLCLIENT $(NoWarn);IDE0017;IDE0034;IDE0037;IDE0039;IDE0042;IDE0044;IDE0051;IDE0052;IDE0059;IDE0060;IDE0063;IDE1006;xUnit1004;CA1806;CA1816;CA1822;CA1825;CA2208;CA1861 enable + true - + $(DefineConstants);ENTITY_FRAMEWORK;LINQ2SQL;OLEDB - + + + + + + - + - + - + + - + + + + + diff --git a/tests/Dapper.Tests/DateTimeOnlyTests.cs b/tests/Dapper.Tests/DateTimeOnlyTests.cs new file mode 100644 index 000000000..cabad699d --- /dev/null +++ b/tests/Dapper.Tests/DateTimeOnlyTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +#if NET6_0_OR_GREATER +namespace Dapper.Tests; + +/* we do **NOT** expect this to work against System.Data +[Collection("DateTimeOnlyTests")] +public sealed class SystemSqlClientDateTimeOnlyTests : DateTimeOnlyTests { } +*/ +#if MSSQLCLIENT && DATEONLY +[Collection("DateTimeOnlyTests")] +public sealed class MicrosoftSqlClientDateTimeOnlyTests : DateTimeOnlyTests { } +#endif +public abstract class DateTimeOnlyTests : TestBase where TProvider : DatabaseProvider +{ + public class HazDateTimeOnly + { + public string Name { get; set; } = ""; + public DateOnly Date { get; set; } + public TimeOnly Time { get; set; } + public DateOnly? NDate { get; set; } + public TimeOnly? NTime { get; set; } + } + + [Fact] + public void TypedInOut() + { + var now = DateTime.Now; + var args = new HazDateTimeOnly + { + Name = nameof(TypedInOut), + Date = DateOnly.FromDateTime(now), + Time = TimeOnly.FromDateTime(now), + NDate = DateOnly.FromDateTime(now), + NTime = TimeOnly.FromDateTime(now), + }; + var row = connection.QuerySingle("select @name as [Name], @date as [Date], @time as [Time], @ndate as [NDate], @ntime as [NTime]", args); + Assert.Equal(args.Name, row.Name); + Assert.Equal(args.Date, row.Date); + Assert.Equal(args.Time, row.Time); + Assert.Equal(args.NDate, row.NDate); + Assert.Equal(args.NTime, row.NTime); + } + + [Fact] + public async Task TypedInOutAsync() + { + var now = DateTime.Now; + var args = new HazDateTimeOnly + { + Name = nameof(TypedInOutAsync), + Date = DateOnly.FromDateTime(now), + Time = TimeOnly.FromDateTime(now), + NDate = DateOnly.FromDateTime(now), + NTime = TimeOnly.FromDateTime(now), + }; + var row = await connection.QuerySingleAsync("select @name as [Name], @date as [Date], @time as [Time], @ndate as [NDate], @ntime as [NTime]", args); + Assert.Equal(args.Name, row.Name); + Assert.Equal(args.Date, row.Date); + Assert.Equal(args.Time, row.Time); + Assert.Equal(args.NDate, row.NDate); + Assert.Equal(args.NTime, row.NTime); + } + + [Fact] + public void UntypedInOut() + { + var now = DateTime.Now; + var args = new DynamicParameters(); + var name = nameof(UntypedInOut); + var date = DateOnly.FromDateTime(now); + var time = TimeOnly.FromDateTime(now); + args.Add("name", name); + args.Add("date", date); + args.Add("time", time); + var row = connection.QuerySingle("select @name as [Name], @date as [Date], @time as [Time]", args); + Assert.Equal(name, (string)row.Name); + // untyped, observation is that these come back as DateTime and TimeSpan + Assert.Equal(date, DateOnly.FromDateTime((DateTime)row.Date)); + Assert.Equal(time, TimeOnly.FromTimeSpan((TimeSpan)row.Time)); + } +} +#endif diff --git a/tests/Dapper.Tests/Helpers/IsExternalInit.cs b/tests/Dapper.Tests/Helpers/IsExternalInit.cs new file mode 100644 index 000000000..7a67b19de --- /dev/null +++ b/tests/Dapper.Tests/Helpers/IsExternalInit.cs @@ -0,0 +1,7 @@ +namespace System.Runtime.CompilerServices; + +#if !NET5_0_OR_GREATER +internal static class IsExternalInit +{ +} +#endif diff --git a/tests/Dapper.Tests/MiscTests.cs b/tests/Dapper.Tests/MiscTests.cs index 9028919ea..077e93647 100644 --- a/tests/Dapper.Tests/MiscTests.cs +++ b/tests/Dapper.Tests/MiscTests.cs @@ -9,7 +9,7 @@ using Microsoft.CSharp.RuntimeBinder; using Xunit; -#if NETCOREAPP3_1 || NET462 || NET472 +#if NET472 namespace System.Runtime.CompilerServices { [EditorBrowsable(EditorBrowsableState.Never)] @@ -265,7 +265,7 @@ static async Task TestExceptionsAsync(DbConnection connection, string sql, st await TestExceptionsAsync( connection, "Select null as Foo", - "Error parsing column 0 (Foo=)"); + "Error parsing column 0 (Foo=n/a - Null object cannot be converted to a value type.)"); // Incompatible value throws (testing unnamed column bits here too) await TestExceptionsAsync( connection, @@ -1309,9 +1309,9 @@ public HazGetOnlyAndCtor(int idProperty, string nameProperty) IdProperty = idProperty; NameProperty = nameProperty; } - } - - [Fact] + } + + [Fact] public void Issue1164_OverflowExceptionForByte() { const string sql = "select cast(200 as smallint) as [value]"; // 200 more than sbyte.MaxValue but less than byte.MaxValue @@ -1345,7 +1345,7 @@ public void Issue1164_OverflowExceptionForUInt64() private class Issue1164Object { - public T Value; + public T Value = default!; } internal record struct One(int OID); @@ -1358,5 +1358,41 @@ public async Task QuerySplitStruct() // https://github.com/DapperLib/Dapper/issu Assert.Single(results); } + + [Fact] + public void SetDynamicProperty_WithReferenceType_Succeeds() + { + var obj = connection.QueryFirst("select 1 as ExistingProperty"); + + obj.ExistingProperty = "foo"; + Assert.Equal("foo", (string)obj.ExistingProperty); + + obj.NewProperty = new Uri("http://example.net/"); + Assert.Equal(new Uri("http://example.net/"), (Uri)obj.NewProperty); + } + + [Fact] + public void SetDynamicProperty_WithBoxedValueType_Succeeds() + { + var obj = connection.QueryFirst("select 'foo' as ExistingProperty"); + + obj.ExistingProperty = (object)1; + Assert.Equal(1, (int)obj.ExistingProperty); + + obj.NewProperty = (object)true; + Assert.True(obj.NewProperty); + } + + [Fact] + public void SetDynamicProperty_WithValueType_Succeeds() + { + var obj = connection.QueryFirst("select 'foo' as ExistingProperty"); + + obj.ExistingProperty = 1; + Assert.Equal(1, (int)obj.ExistingProperty); + + obj.NewProperty = true; + Assert.True(obj.NewProperty); + } } } diff --git a/tests/Dapper.Tests/ParameterTests.cs b/tests/Dapper.Tests/ParameterTests.cs index b6408359e..5eb455c65 100644 --- a/tests/Dapper.Tests/ParameterTests.cs +++ b/tests/Dapper.Tests/ParameterTests.cs @@ -17,6 +17,8 @@ using Microsoft.SqlServer.Types; #endif +[assembly: CollectionBehavior(DisableTestParallelization = true)] + namespace Dapper.Tests { [Collection(NonParallelDefinition.Name)] // because it creates SQL types that compete between the two providers @@ -59,18 +61,39 @@ public void AddParameter(IDbCommand command, string name) } } + public readonly struct DbCustomParamStruct : SqlMapper.ICustomQueryParameter + { + private readonly IDbDataParameter _sqlParameter; + + public DbCustomParamStruct(IDbDataParameter sqlParameter) + { + _sqlParameter = sqlParameter; + } + + public void AddParameter(IDbCommand command, string name) + { + command.Parameters.Add(_sqlParameter); + } + } + private static IEnumerable CreateSqlDataRecordList(IDbCommand command, IEnumerable numbers) { +#pragma warning disable CS0618 // Type or member is obsolete if (command is System.Data.SqlClient.SqlCommand) return CreateSqlDataRecordList_SD(numbers); +#pragma warning restore CS0618 // Type or member is obsolete if (command is Microsoft.Data.SqlClient.SqlCommand) return CreateSqlDataRecordList_MD(numbers); throw new ArgumentException(nameof(command)); } private static IEnumerable CreateSqlDataRecordList(IDbConnection connection, IEnumerable numbers) { +#pragma warning disable CS0618 // Type or member is obsolete if (connection is System.Data.SqlClient.SqlConnection) return CreateSqlDataRecordList_SD(numbers); +#pragma warning restore CS0618 // Type or member is obsolete if (connection is Microsoft.Data.SqlClient.SqlConnection) return CreateSqlDataRecordList_MD(numbers); throw new ArgumentException(nameof(connection)); } + +#pragma warning disable CS0618 // Type or member is obsolete private static List CreateSqlDataRecordList_SD(IEnumerable numbers) { var number_list = new List(); @@ -88,6 +111,7 @@ private static IEnumerable CreateSqlDataRecordList(IDbConnection co return number_list; } +#pragma warning restore CS0618 // Type or member is obsolete private static List CreateSqlDataRecordList_MD(IEnumerable numbers) { @@ -147,6 +171,7 @@ public void AddParameter(IDbCommand command, string name) private static IDbDataParameter AddStructured(IDbCommand command, object value) { +#pragma warning disable CS0618 // Type or member is obsolete if (command is System.Data.SqlClient.SqlCommand sdcmd) { var p = sdcmd.Parameters.Add("integers", SqlDbType.Structured); @@ -155,6 +180,7 @@ private static IDbDataParameter AddStructured(IDbCommand command, object value) p.Value = value; return p; } +#pragma warning restore CS0618 // Type or member is obsolete else if (command is Microsoft.Data.SqlClient.SqlCommand mdcmd) { var p = mdcmd.Parameters.Add("integers", SqlDbType.Structured); @@ -497,11 +523,13 @@ public void TestSqlDataRecordListParametersWithTypeHandlers() // Variable type has to be IEnumerable for TypeHandler to kick in. object args; +#pragma warning disable CS0618 // Type or member is obsolete if (connection is System.Data.SqlClient.SqlConnection) { IEnumerable records = CreateSqlDataRecordList_SD(new int[] { 1, 2, 3 }); args = new { integers = records }; } +#pragma warning restore CS0618 // Type or member is obsolete else if (connection is Microsoft.Data.SqlClient.SqlConnection) { IEnumerable records = CreateSqlDataRecordList_MD(new int[] { 1, 2, 3 }); @@ -872,8 +900,23 @@ public void TestCustomParameterReuse() Assert.Equal(123, result2.Foo); Assert.Equal("abc", result2.Bar); } - - + + [Fact] + public void TestCustomParameterValueType() + { + // Value type (struct) ICustomQueryParameter previously caused a segfault + // because the IL emitted Callvirt on an unboxed struct (see #2189) + var args = new { + foo = new DbCustomParamStruct(Provider.CreateRawParameter("foo", 123)), + bar = "abc" + }; + var result = connection.Query("select Foo=@foo, Bar=@bar", args).Single(); + int foo = result.Foo; + string bar = result.Bar; + Assert.Equal(123, foo); + Assert.Equal("abc", bar); + } + [Fact] public void TestDynamicParamNullSupport() { @@ -1655,10 +1698,12 @@ Id int not null primary key identity(1,1), { recvValue = msReader.GetSqlDecimal(1); } +#pragma warning disable CS0618 // Type or member is obsolete else if (reader is System.Data.SqlClient.SqlDataReader sdReader) { recvValue = sdReader.GetSqlDecimal(1); } +#pragma warning restore CS0618 // Type or member is obsolete else { throw new InvalidOperationException($"unexpected reader type: {reader.GetType().FullName}"); diff --git a/tests/Dapper.Tests/PreferTypeHandlersForEnumsTests.cs b/tests/Dapper.Tests/PreferTypeHandlersForEnumsTests.cs new file mode 100644 index 000000000..958f0d546 --- /dev/null +++ b/tests/Dapper.Tests/PreferTypeHandlersForEnumsTests.cs @@ -0,0 +1,112 @@ +using System; +using System.Data; +using System.Linq; +using Xunit; + +namespace Dapper.Tests +{ + [Collection(NonParallelDefinition.Name)] + public sealed class SystemSqlClientPreferTypeHandlersForEnumsTests : PreferTypeHandlersForEnumsTests { } +#if MSSQLCLIENT + [Collection(NonParallelDefinition.Name)] + public sealed class MicrosoftSqlClientPreferTypeHandlersForEnumsTests : PreferTypeHandlersForEnumsTests { } +#endif + + public abstract class PreferTypeHandlersForEnumsTests : TestBase where TProvider : DatabaseProvider + { + private enum Color + { + Red = 1, + Green = 2, + Blue = 3 + } + + private class ColorResult + { + public Color Value { get; set; } + } + + private class NullableColorResult + { + public Color? Value { get; set; } + } + + /// + /// A TypeHandler that stores enum values as their name strings + /// and parses them back from strings. + /// + private class StringEnumHandler : SqlMapper.TypeHandler where TEnum : struct, Enum + { + public static readonly StringEnumHandler Instance = new(); + public int ParseCallCount; + public int SetValueCallCount; + + public override TEnum Parse(object? value) + { + ParseCallCount++; + return (TEnum)Enum.Parse(typeof(TEnum), (string)value!); + } + + public override void SetValue(IDbDataParameter parameter, TEnum value) + { + SetValueCallCount++; + parameter.DbType = DbType.AnsiString; + parameter.Value = value.ToString(); + } + } + + [Fact] + public void EnumTypeHandler_WriteAndRead_UsesHandlerWhenEnabled() + { + var handler = new StringEnumHandler(); + var oldSetting = SqlMapper.Settings.PreferTypeHandlersForEnums; + try + { + SqlMapper.ResetTypeHandlers(); + SqlMapper.AddTypeHandler(typeof(Color), handler); + SqlMapper.Settings.PreferTypeHandlersForEnums = true; + SqlMapper.PurgeQueryCache(); + + // Round-trip: write as string, read back via handler + var result = connection.Query( + "SELECT @Value AS Value", new { Value = Color.Green }).Single(); + + Assert.Equal(Color.Green, result.Value); + Assert.True(handler.SetValueCallCount > 0, "SetValue should have been called"); + Assert.True(handler.ParseCallCount > 0, "Parse should have been called"); + } + finally + { + SqlMapper.Settings.PreferTypeHandlersForEnums = oldSetting; + SqlMapper.ResetTypeHandlers(); + SqlMapper.PurgeQueryCache(); + } + } + + [Fact] + public void EnumTypeHandler_NullableWithNull_ReturnsNull() + { + var handler = new StringEnumHandler(); + var oldSetting = SqlMapper.Settings.PreferTypeHandlersForEnums; + try + { + SqlMapper.ResetTypeHandlers(); + SqlMapper.AddTypeHandler(typeof(Color), handler); + SqlMapper.Settings.PreferTypeHandlersForEnums = true; + SqlMapper.PurgeQueryCache(); + + Color? input = null; + var result = connection.Query( + "SELECT @Value AS Value", new { Value = input }).Single(); + + Assert.Null(result.Value); + } + finally + { + SqlMapper.Settings.PreferTypeHandlersForEnums = oldSetting; + SqlMapper.ResetTypeHandlers(); + SqlMapper.PurgeQueryCache(); + } + } + } +} diff --git a/tests/Dapper.Tests/ProviderTests.cs b/tests/Dapper.Tests/ProviderTests.cs index e3a553c5f..c0f2d58f2 100644 --- a/tests/Dapper.Tests/ProviderTests.cs +++ b/tests/Dapper.Tests/ProviderTests.cs @@ -10,8 +10,10 @@ public class ProviderTests [Fact] public void BulkCopy_SystemDataSqlClient() { +#pragma warning disable CS0618 // Type or member is obsolete using var conn = new System.Data.SqlClient.SqlConnection(); Test(conn); +#pragma warning restore CS0618 // Type or member is obsolete } [Fact] diff --git a/tests/Dapper.Tests/Providers/SnowflakeTests.cs b/tests/Dapper.Tests/Providers/SnowflakeTests.cs index 64b2d11f7..610592461 100644 --- a/tests/Dapper.Tests/Providers/SnowflakeTests.cs +++ b/tests/Dapper.Tests/Providers/SnowflakeTests.cs @@ -1,4 +1,4 @@ -#if !NET462 // platform not supported exception +#if !NETFRAMEWORK // platform not supported exception using System; using System.Collections.Generic; using System.IO; diff --git a/tests/Dapper.Tests/SingleRowTests.cs b/tests/Dapper.Tests/SingleRowTests.cs new file mode 100644 index 000000000..ab554514f --- /dev/null +++ b/tests/Dapper.Tests/SingleRowTests.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FastMember; +using Xunit; +using Xunit.Abstractions; +using static Dapper.SqlMapper; + +namespace Dapper.Tests; + +[Collection("SingleRowTests")] +public sealed class SystemSqlClientSingleRowTests(ITestOutputHelper log) : SingleRowTests(log) +{ + protected override async Task InjectDataAsync(DbConnection conn, DbDataReader source) + { +#pragma warning disable CS0618 // Type or member is obsolete + using var bcp = new System.Data.SqlClient.SqlBulkCopy((System.Data.SqlClient.SqlConnection)conn); +#pragma warning restore CS0618 // Type or member is obsolete + bcp.DestinationTableName = "#mydata"; + bcp.EnableStreaming = true; + await bcp.WriteToServerAsync(source); + } +} +#if MSSQLCLIENT +[Collection("SingleRowTests")] +public sealed class MicrosoftSqlClientSingleRowTests(ITestOutputHelper log) : SingleRowTests(log) +{ + protected override async Task InjectDataAsync(DbConnection conn, DbDataReader source) + { + using var bcp = new Microsoft.Data.SqlClient.SqlBulkCopy((Microsoft.Data.SqlClient.SqlConnection)conn); + bcp.DestinationTableName = "#mydata"; + bcp.EnableStreaming = true; + await bcp.WriteToServerAsync(source); + } +} +#endif +public abstract class SingleRowTests(ITestOutputHelper log) : TestBase where TProvider : DatabaseProvider +{ + protected abstract Task InjectDataAsync(DbConnection connection, DbDataReader source); + + [Fact] + public async Task QueryFirst_PerformanceAndCorrectness() + { + using var conn = GetOpenConnection(); + conn.Execute("create table #mydata(id int not null, name nvarchar(250) not null)"); + + var rand = new Random(); + var data = from id in Enumerable.Range(1, 500_000) + select new MyRow { Id = rand.Next(), Name = CreateName(rand) }; + + Stopwatch watch; + using (var reader = ObjectReader.Create(data)) + { + await InjectDataAsync(conn, reader); + watch = Stopwatch.StartNew(); + var count = await conn.QuerySingleAsync("""select count(1) from #mydata"""); + watch.Stop(); + log.WriteLine($"bulk-insert complete; {count} rows in {watch.ElapsedMilliseconds}ms"); + } + + // just errors + var ex = Assert.ThrowsAny(() => conn.Execute("raiserror('bad things', 16, 1)")); + log.WriteLine(ex.Message); + ex = await Assert.ThrowsAnyAsync(async () => await conn.ExecuteAsync("raiserror('bad things', 16, 1)")); + log.WriteLine(ex.Message); + + // just data + watch = Stopwatch.StartNew(); + var row = conn.QueryFirst("select top 1 * from #mydata"); + watch.Stop(); + log.WriteLine($"sync top 1 read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms"); + + watch = Stopwatch.StartNew(); + row = await conn.QueryFirstAsync("select top 1 * from #mydata"); + watch.Stop(); + log.WriteLine($"async top 1 read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms"); + + watch = Stopwatch.StartNew(); + row = conn.QueryFirst("select * from #mydata"); + watch.Stop(); + log.WriteLine($"sync read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms"); + + watch = Stopwatch.StartNew(); + row = await conn.QueryFirstAsync("select * from #mydata"); + watch.Stop(); + log.WriteLine($"async read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms"); + + // data with trailing errors + + watch = Stopwatch.StartNew(); + ex = Assert.ThrowsAny(() => conn.QueryFirst("select * from #mydata; raiserror('bad things', 16, 1)")); + watch.Stop(); + log.WriteLine($"sync read with error complete in {watch.ElapsedMilliseconds}ms; {ex.Message}"); + + watch = Stopwatch.StartNew(); + ex = await Assert.ThrowsAnyAsync(async () => await conn.QueryFirstAsync("select * from #mydata; raiserror('bad things', 16, 1)")); + watch.Stop(); + log.WriteLine($"async read with error complete in {watch.ElapsedMilliseconds}ms; {ex.Message}"); + + // unbuffered read with trailing errors - do not expect to see this unless we consume all! + + watch = Stopwatch.StartNew(); + row = conn.Query("select * from #mydata", buffered: false).First(); + watch.Stop(); + log.WriteLine($"sync unbuffered LINQ read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms"); + + watch = Stopwatch.StartNew(); + row = await conn.QueryUnbufferedAsync("select * from #mydata").FirstAsync(); + watch.Stop(); + log.WriteLine($"async unbuffered LINQ read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms"); + + static unsafe string CreateName(Random rand) + { + const string Alphabet = "abcdefghijklmnopqrstuvwxyz 0123456789,;-"; + var len = rand.Next(5, 251); + char* ptr = stackalloc char[len]; + for (int i = 0; i < len; i++) + { + ptr[i] = Alphabet[rand.Next(Alphabet.Length)]; + } + return new string(ptr, 0, len); + } + + } + + public class MyRow + { + public int Id { get; set; } + public string Name { get; set; } = ""; + } +} + +internal static class AsyncLinqHelper +{ + public static async ValueTask FirstAsync(this IAsyncEnumerable source, CancellationToken cancellationToken = default) + { + await using var iter = source.GetAsyncEnumerator(cancellationToken); + if (!await iter.MoveNextAsync()) Array.Empty().First(); // for consistent error + return iter.Current; + } +} diff --git a/tests/Dapper.Tests/TestBase.cs b/tests/Dapper.Tests/TestBase.cs index 7c7b2920f..e5e2c6560 100644 --- a/tests/Dapper.Tests/TestBase.cs +++ b/tests/Dapper.Tests/TestBase.cs @@ -74,7 +74,9 @@ public DbConnection GetOpenConnection(bool mars) } public sealed class SystemSqlClientProvider : SqlServerDatabaseProvider { +#pragma warning disable CS0618 // Type or member is obsolete public override DbProviderFactory Factory => System.Data.SqlClient.SqlClientFactory.Instance; +#pragma warning restore CS0618 // Type or member is obsolete } #if MSSQLCLIENT public sealed class MicrosoftSqlClientProvider : SqlServerDatabaseProvider diff --git a/tests/Dapper.Tests/TypeHandlerTests.cs b/tests/Dapper.Tests/TypeHandlerTests.cs index 2e754ca46..385ffb90b 100644 --- a/tests/Dapper.Tests/TypeHandlerTests.cs +++ b/tests/Dapper.Tests/TypeHandlerTests.cs @@ -28,12 +28,18 @@ public void TestChangingDefaultStringTypeMappingToAnsiString() SqlMapper.PurgeQueryCache(); - SqlMapper.AddTypeMap(typeof(string), DbType.AnsiString); // Change Default String Handling to AnsiString - var result02 = connection.Query(sql, param).FirstOrDefault(); - Assert.Equal("varchar", result02); + SqlMapper.AddTypeMap(typeof(string), DbType.AnsiString, false); // Change Default String Handling to AnsiString + try + { + var result02 = connection.Query(sql, param).FirstOrDefault(); + Assert.Equal("varchar", result02); - SqlMapper.PurgeQueryCache(); - SqlMapper.AddTypeMap(typeof(string), DbType.String); // Restore Default to Unicode String + SqlMapper.PurgeQueryCache(); + } + finally + { + SqlMapper.AddTypeMap(typeof(string), DbType.String, false); // Restore Default to Unicode String + } } [Fact] @@ -46,13 +52,18 @@ public void TestChangingDefaultStringTypeMappingToAnsiStringFirstOrDefault() Assert.Equal("nvarchar", result01); SqlMapper.PurgeQueryCache(); + SqlMapper.AddTypeMap(typeof(string), DbType.AnsiString, false); // Change Default String Handling to AnsiString + try + { + var result02 = connection.QueryFirstOrDefault(sql, param); + Assert.Equal("varchar", result02); - SqlMapper.AddTypeMap(typeof(string), DbType.AnsiString); // Change Default String Handling to AnsiString - var result02 = connection.QueryFirstOrDefault(sql, param); - Assert.Equal("varchar", result02); - - SqlMapper.PurgeQueryCache(); - SqlMapper.AddTypeMap(typeof(string), DbType.String); // Restore Default to Unicode String + SqlMapper.PurgeQueryCache(); + } + finally + { + SqlMapper.AddTypeMap(typeof(string), DbType.String, false); // Restore Default to Unicode String + } } [Fact] @@ -643,7 +654,7 @@ public void Issue149_TypeMismatch_SequentialAccess() { Guid guid = Guid.Parse("cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e"); var ex = Assert.ThrowsAny(() => connection.Query("select @guid as Id", new { guid }).First()); - Assert.Equal("Error parsing column 0 (Id=cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e - Object)", ex.Message); + Assert.Equal("Error parsing column 0 (Id=cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e - Guid)", ex.Message); } public class Issue149_Person { public string? Id { get; set; } } diff --git a/tests/Dapper.Tests/WrappedReaderTests.cs b/tests/Dapper.Tests/WrappedReaderTests.cs new file mode 100644 index 000000000..7f884f420 --- /dev/null +++ b/tests/Dapper.Tests/WrappedReaderTests.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections; +using System.Data; +using System.Data.Common; +using Xunit.Abstractions; + +namespace Dapper.Tests; + +public class WrappedReaderTests(ITestOutputHelper testOutputHelper) +{ + [Fact] + public void DbWrappedReader_Dispose_DoesNotThrow() + { + var reader = new DbWrappedReader(new DummyDbCommand(), new ThrowOnCloseDbDataReader(testOutputHelper)); + reader.Dispose(); + } + +#if !NETFRAMEWORK + [Fact] + public async System.Threading.Tasks.Task DbWrappedReader_DisposeAsync_DoesNotThrow() + { + var reader = new DbWrappedReader(new DummyDbCommand(), new ThrowOnCloseDbDataReader(testOutputHelper)); + await reader.DisposeAsync(); + } +#endif + + [Fact] + public void WrappedBasicReader_Dispose_DoesNotThrow() + { + var reader = new WrappedBasicReader(new ThrowOnCloseIDataReader()); + reader.Dispose(); + } + +#if !NETFRAMEWORK + [Fact] + public async System.Threading.Tasks.Task WrappedBasicReader_DisposeAsync_DoesNotThrow() + { + var reader = new WrappedBasicReader(new ThrowOnCloseIDataReader()); + await reader.DisposeAsync(); + } +#endif + + private class DummyDbCommand : DbCommand + { + public override void Cancel() => throw new NotSupportedException(); + public override int ExecuteNonQuery() => throw new NotSupportedException(); + public override object ExecuteScalar() => throw new NotSupportedException(); + public override void Prepare() => throw new NotSupportedException(); + +#pragma warning disable CS8765 // nullability of value + public override string CommandText { get; set; } = ""; +#pragma warning restore CS8765 // nullability of value + public override int CommandTimeout { get; set; } + public override CommandType CommandType { get; set; } + public override UpdateRowSource UpdatedRowSource { get; set; } + protected override DbConnection? DbConnection { get; set; } + protected override DbParameterCollection DbParameterCollection => throw new NotSupportedException(); + protected override DbTransaction? DbTransaction { get; set; } + public override bool DesignTimeVisible { get; set; } + protected override DbParameter CreateDbParameter() => throw new NotSupportedException(); + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw new NotSupportedException(); + } + + private class DummyDbException(string message) : DbException(message); + + private class ThrowOnCloseDbDataReader(ITestOutputHelper testOutputHelper) : DbDataReader + { + // This is basically what SqlClient does, see https://github.com/dotnet/SqlClient/blob/v5.2.1/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs#L835-L849 + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + Close(); + } + base.Dispose(disposing); + } + catch (DbException e) + { + testOutputHelper.WriteLine($"Ignored exception when disposing {e}"); + } + } + + public override void Close() => throw new DummyDbException("Exception during Close()"); + + public override bool GetBoolean(int ordinal) => throw new NotSupportedException(); + public override byte GetByte(int ordinal) => throw new NotSupportedException(); + public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) => throw new NotSupportedException(); + public override char GetChar(int ordinal) => throw new NotSupportedException(); + public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) => throw new NotSupportedException(); + public override string GetDataTypeName(int ordinal) => throw new NotSupportedException(); + public override DateTime GetDateTime(int ordinal) => throw new NotSupportedException(); + public override decimal GetDecimal(int ordinal) => throw new NotSupportedException(); + public override double GetDouble(int ordinal) => throw new NotSupportedException(); + public override Type GetFieldType(int ordinal) => throw new NotSupportedException(); + public override float GetFloat(int ordinal) => throw new NotSupportedException(); + public override Guid GetGuid(int ordinal) => throw new NotSupportedException(); + public override short GetInt16(int ordinal) => throw new NotSupportedException(); + public override int GetInt32(int ordinal) => throw new NotSupportedException(); + public override long GetInt64(int ordinal) => throw new NotSupportedException(); + public override string GetName(int ordinal) => throw new NotSupportedException(); + public override int GetOrdinal(string name) => throw new NotSupportedException(); + public override string GetString(int ordinal) => throw new NotSupportedException(); + public override object GetValue(int ordinal) => throw new NotSupportedException(); + public override int GetValues(object[] values) => throw new NotSupportedException(); + public override bool IsDBNull(int ordinal) => throw new NotSupportedException(); + public override int FieldCount => throw new NotSupportedException(); + public override object this[int ordinal] => throw new NotSupportedException(); + public override object this[string name] => throw new NotSupportedException(); + public override int RecordsAffected => throw new NotSupportedException(); + public override bool HasRows => throw new NotSupportedException(); + public override bool IsClosed => throw new NotSupportedException(); + public override bool NextResult() => throw new NotSupportedException(); + public override bool Read() => throw new NotSupportedException(); + public override int Depth => throw new NotSupportedException(); + public override IEnumerator GetEnumerator() => throw new NotSupportedException(); + } + + private class ThrowOnCloseIDataReader : IDataReader + { + public void Dispose() + { + // Assume that IDataReader Dispose implementation does not throw + } + + public void Close() => throw new DummyDbException("Exception during Close()"); + + public bool GetBoolean(int i) => throw new NotSupportedException(); + public byte GetByte(int i) => throw new NotSupportedException(); + public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => throw new NotSupportedException(); + public char GetChar(int i) => throw new NotSupportedException(); + public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => throw new NotSupportedException(); + public IDataReader GetData(int i) => throw new NotSupportedException(); + public string GetDataTypeName(int i) => throw new NotSupportedException(); + public DateTime GetDateTime(int i) => throw new NotSupportedException(); + public decimal GetDecimal(int i) => throw new NotSupportedException(); + public double GetDouble(int i) => throw new NotSupportedException(); + public Type GetFieldType(int i) => throw new NotSupportedException(); + public float GetFloat(int i) => throw new NotSupportedException(); + public Guid GetGuid(int i) => throw new NotSupportedException(); + public short GetInt16(int i) => throw new NotSupportedException(); + public int GetInt32(int i) => throw new NotSupportedException(); + public long GetInt64(int i) => throw new NotSupportedException(); + public string GetName(int i) => throw new NotSupportedException(); + public int GetOrdinal(string name) => throw new NotSupportedException(); + public string GetString(int i) => throw new NotSupportedException(); + public object GetValue(int i) => throw new NotSupportedException(); + public int GetValues(object[] values) => throw new NotSupportedException(); + public bool IsDBNull(int i) => throw new NotSupportedException(); + public int FieldCount => throw new NotSupportedException(); + public object this[int i] => throw new NotSupportedException(); + public object this[string name] => throw new NotSupportedException(); + public DataTable? GetSchemaTable() => throw new NotSupportedException(); + public bool NextResult() => throw new NotSupportedException(); + public bool Read() => throw new NotSupportedException(); + public int Depth => throw new NotSupportedException(); + public bool IsClosed => throw new NotSupportedException(); + public int RecordsAffected => throw new NotSupportedException(); + } +}