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 AppProvider-agnostic ADO.NET helper utilitiesMarc Gravell
- net461;netstandard2.0;net5.0
+ net461;netstandard2.0;net8.0trueenable
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 AppThe Dapper SqlBuilder component, for building SQL queries dynamically.Sam Saffron, Johan Danforth
- net461;netstandard2.0;net5.0
+ net461;netstandard2.0;net8.0falsefalseenable
@@ -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.StrongNameorm;sql;micro-ormCodestin 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.0truetrueenable
@@ -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 @@
DapperCodestin Search Apporm;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.0enabletrue
@@ -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
+#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 @@
githttps://github.com/DapperLib/Dapperfalse
- $(NOWARN);IDE0056;IDE0057;IDE0079
+ $(NOWARN);IDE0056;IDE0057;IDE0079;DX1001trueembeddeden-USfalsetruetrue
- 12
+ 13falsetruereadme.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)
+
+
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.PerformanceDapper Core Performance SuiteExe
- net462;net8.0
+ net472;net10.0false$(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.
+
+
\ 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)
+
+[](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.TestsDapper 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;CA1861enable
+ 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();
+ }
+}