diff --git a/.agents/README.md b/.agents/README.md
index ce6cb95..eb52a69 100644
--- a/.agents/README.md
+++ b/.agents/README.md
@@ -5,6 +5,7 @@ Reusable AI-agent instructions have one canonical location:
```text
AGENTS.md Repository guidance
.agents/skills/*/SKILL.md Workflow procedures
+.agents/references/*.md Reference material AGENTS.md links to
.agents/references/reviews/*.md Review checklists
scripts/*.ps1 Executable checks and workflows
```
@@ -26,7 +27,12 @@ Codex-only UI metadata and explicit-invocation policy. Claude needs thin skill w
`disable-model-invocation` policy lives in `SKILL.md` frontmatter.
The hook adapters differ because Claude and Codex use different payload and response contracts. Both delegate
-all substantive behavior to the same scripts.
+all substantive behavior to the same scripts: `scripts/tidy-cs.ps1` and `scripts/public-api-guard.ps1`.
+
+The tidy hook runs the **default scope only** — CSharpier, under a second. Code style and member ordering are
+not run on every edit: `dotnet format style` needs MSBuild and ReSharper loads the whole solution, and neither
+belongs on the critical path of a single edit. All three are build errors, and `scripts/preflight.ps1` runs
+`tidy-cs.ps1 -Scope all` before a commit, so nothing reaches a pull request untidied.
When changing behavior, edit the canonical file. Keep only required names, descriptions, policies, tool/model
settings, and reference instructions in tool-specific files.
diff --git a/.agents/references/code-style.md b/.agents/references/code-style.md
new file mode 100644
index 0000000..083472b
--- /dev/null
+++ b/.agents/references/code-style.md
@@ -0,0 +1,116 @@
+# Code style details
+
+Background for the rules in [AGENTS.md](../../AGENTS.md#code-style-formatting-and-ordering). Read this when a
+tool does something you did not expect, or when you are about to write a type name in a place the build does
+not check.
+
+## Three concerns, three tools
+
+| Concern | Tool | Configured in |
+|---|---|---|
+| Formatting — whitespace, line breaks, wrapping | **CSharpier** | `.editorconfig` (`max_line_length`, `indent_size`) |
+| Style — `var`, `=>`, `this.`, null checks, usings | **Roslyn analyzers** | `.editorconfig` |
+| Ordering — types and their members | **ReSharper** applies it, **NewStyleCop** checks it | `DbConnectionPlus.slnx.DotSettings` and `stylecop.json` |
+
+Each tool owns its concern completely, and all three are build errors rather than warnings, in `tests/` and
+`benchmarks/` as much as in `src/`. Two different mechanisms, both in the root `Directory.Build.props`:
+`EnforceCodeStyleInBuild=true` with `TreatWarningsAsErrors=true` covers style and ordering, and the
+`CSharpier.MsBuild` package covers formatting. It runs in check mode, so a build never rewrites your files —
+an unformatted file fails the build and names itself. `-p:CSharpier_Bypass=true` skips it.
+
+## Where the build misses a BCL type name
+
+The rule is: write `string`, `object?`, `int`, `bool`, `nint`, `nuint` — never `String`, `Object?`, `Int32`,
+`Boolean`, `IntPtr`, `UIntPtr`. `dotnet_style_predefined_type_for_*` is `true:error`, and `IDE0049` enforces
+most of it.
+
+`IDE0049` has two blind spots. Neither produces a diagnostic, so `TreatWarningsAsErrors` has nothing to fail
+on. Both were verified against the analyzer:
+
+- **`nint`/`nuint` are not on its list.** They arrived in C# 9 as their own feature and only became aliases
+ for `IntPtr`/`UIntPtr` in C# 11; the analyzer was never extended. `IntPtr` is invisible to it.
+- **It never looks inside `nameof(...)`.** With good reason: `nameof(int)` does not compile at all
+ (CS1525 — a keyword is not a name), so a blanket skip is the safe choice. Where `nameof` names a CLR type
+ on purpose, as in `EnumerableReaderTests`, leave it and say why in a comment.
+
+A third gap is not the analyzer's fault: **`tests/package-consumption/` is not in `DbConnectionPlus.slnx`**,
+so `dotnet format` and `dotnet build` on the solution never see it, and its deliberately empty
+`Directory.Build.props` means it gets no style gate even when `verify-package-aot.ps1` builds it. If you touch
+those files, apply the style by hand.
+
+## Primary constructor parameters
+
+Every primary constructor parameter is assigned to a `private readonly` backing field, and members read
+`this.field` rather than the parameter:
+
+```csharp
+internal class SqliteEntityManipulator(SqliteDatabaseAdapter databaseAdapter) : IEntityManipulator
+{
+ private readonly SqliteDatabaseAdapter databaseAdapter = databaseAdapter;
+```
+
+Using the parameter directly is shorter and looks equivalent. It is not. A parameter that a member body reads
+is *captured*, and the field the compiler generates for it carries no `readonly` — so the value becomes
+reassignable from inside the type, where the explicit field would have made that a compile error. C# has no way
+to mark a primary constructor parameter `readonly`, so the backing field is the only way to keep the guarantee.
+
+There is no diagnostic for this. `IDE0290` asks for the primary constructor and stops there; the `this.`
+rules cannot see a parameter at all. It is on the author.
+
+The field costs nothing at runtime: a parameter used only in a field initializer is not captured, so the
+explicit field and the compiler's capture field are one field, not two.
+
+## When a tool surprises you
+
+- **`cleanupcode` re-indents the content of raw string literals and CSharpier puts it back.** Neither is
+ idempotent alone; the pair is. Always let CSharpier run last — `tidy-cs.ps1` does.
+- **Explicit interface implementations sort first — except events.** StyleCop counts an explicit property,
+ indexer or method as public, so each of those has its own "Explicit interface …" entry in the file layout
+ putting it at the front of its group. An explicit **event** is counted as private, so it stays where
+ ReSharper puts it by default, at the end; giving events an entry breaks the build. The entries match
+ `ImplementsInterface` **and** `Access Is="Private"` — without the access test they would also catch implicit
+ implementations and pull `Equals(T)` away from `Equals(object)`.
+- **Overloads with the same name have no defined order between them.** Two methods called `Equals` tie on
+ every key the file layout sorts by — kind, access, static, readonly and name — and ReSharper's sort is
+ stable, so it leaves them in whatever order it found them. Reordering the same file starting from two
+ different states can therefore give two different results, and both are correct: StyleCop accepts either,
+ and moving a method declaration changes nothing at runtime. CI does not flap over it, because a stable sort
+ leaves the committed order alone. There is no fix available — ReSharper's `SortBy` offers `` but
+ nothing that separates overloads by signature. Put new overloads next to their siblings and it will never
+ come up.
+
+## XML documentation comments
+
+XML docs go on all public and most internal members of the **shipping** projects — ``, ``,
+``, ``. Match the density of the file you are editing; `DbConnectionExtensions.QueryFirst.cs`
+sets the bar. Invalid XML docs break the docfx workflow.
+
+The benchmarks are the exception: nothing consumes them as an API, so they use plain `//` comments and switch
+`RCS1181` off for that reason.
+
+## Member order
+
+StyleCop's order, applied by ReSharper and checked by NewStyleCop. Write a new member straight into the right
+place rather than relying on the fixer:
+
+ constants → fields → constructors → finalizers → delegates → events → enums → interfaces
+ → properties → indexers → conversion operators → operators → methods → nested structs → nested classes
+
+Within each of those groups: public before internal before protected before private, static before instance,
+readonly before mutable, and **alphabetical** after that.
+
+Note that fields go at the **top** of a type, and that explicit interface implementations sort ahead of the
+ordinary methods — with the exception for events described above.
+
+## Rules the build fully enforces
+
+These need no attention beyond letting the tools run — they are listed here so that the rule set in AGENTS.md
+stays short, not because they are optional:
+
+- **Primary constructors** wherever `IDE0290` asks for one.
+- **Expression-bodied members** are `error`-severity for methods, constructors, operators, properties, indexers,
+ accessors, lambdas and local functions. Use `=>` wherever a member is a single expression.
+- **File-scoped namespaces**, with usings outside the namespace.
+- **Max line length is 120**, set once in `.editorconfig` and read from there by CSharpier. Do not wrap lines by
+ hand — write it on one line and let CSharpier break it.
+- Nullable and `ImplicitUsings` are enabled; common namespaces come from `GlobalUsings.cs`.
diff --git a/.claude/hooks/format-cs.ps1 b/.claude/hooks/format-cs.ps1
deleted file mode 100644
index d0f78c8..0000000
--- a/.claude/hooks/format-cs.ps1
+++ /dev/null
@@ -1,31 +0,0 @@
-# PostToolUse hook: format an edited C# file against .editorconfig.
-#
-# The formatting logic itself lives in scripts/format-cs.ps1, so that Codex's hook and a human run
-# exactly the same thing. This file is only the hook wiring: read the tool payload off stdin, pull the
-# edited path out of it, and delegate.
-#
-# Never fails the edit - a formatter problem is surfaced as text and the hook still exits 0.
-
-$ErrorActionPreference = 'Stop'
-
-try {
- $payload = [Console]::In.ReadToEnd() | ConvertFrom-Json
- $filePath = $payload.tool_input.file_path
-
- if ([String]::IsNullOrWhiteSpace($filePath)) { exit 0 }
- if ([System.IO.Path]::GetExtension($filePath) -ne '.cs') { exit 0 }
- if (-not (Test-Path -LiteralPath $filePath)) { exit 0 }
-
- $script = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'scripts/format-cs.ps1'
- if (-not (Test-Path -LiteralPath $script)) {
- Write-Output "format-cs hook: scripts/format-cs.ps1 not found at $script"
- exit 0
- }
-
- & $script -Path $filePath
-}
-catch {
- Write-Output "format-cs hook error: $($_.Exception.Message)"
-}
-
-exit 0
diff --git a/.claude/hooks/tidy-cs.ps1 b/.claude/hooks/tidy-cs.ps1
new file mode 100644
index 0000000..b1f45ed
--- /dev/null
+++ b/.claude/hooks/tidy-cs.ps1
@@ -0,0 +1,42 @@
+# PostToolUse hook: format an edited C# file with CSharpier.
+#
+# The logic itself lives in scripts/tidy-cs.ps1, so that Codex's hook and a human run exactly the same
+# thing. This file is only the hook wiring: read the tool payload off stdin, pull the edited path out of
+# it, and delegate.
+#
+# Formatting only, which is the default scope and takes under a second. Style and member ordering are
+# not run here: `dotnet format style` needs MSBuild and ReSharper loads the whole solution, and neither
+# belongs on the critical path of every single edit. All three are build errors, and scripts/preflight.ps1
+# runs `-Scope all` before a commit, so nothing slips through.
+#
+# Never fails the edit - a formatter problem is surfaced as text and the hook still exits 0.
+
+$ErrorActionPreference = 'Stop'
+
+try {
+ $payload = [Console]::In.ReadToEnd() | ConvertFrom-Json
+ $filePath = $payload.tool_input.file_path
+
+ if ([String]::IsNullOrWhiteSpace($filePath)) { exit 0 }
+ if ([System.IO.Path]::GetExtension($filePath) -ne '.cs') { exit 0 }
+ if (-not (Test-Path -LiteralPath $filePath)) { exit 0 }
+
+ $script = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'scripts/tidy-cs.ps1'
+ if (-not (Test-Path -LiteralPath $script)) {
+ Write-Output "tidy-cs hook: scripts/tidy-cs.ps1 not found at $script"
+ exit 0
+ }
+
+ $output = & $script -Path $filePath 2>&1 | Out-String
+
+ # Quiet on success. On failure say so explicitly and say what it means, because the build treats
+ # formatting as an error - the same contract Codex's adapter reports through additionalContext.
+ if ($LASTEXITCODE -ne 0) {
+ Write-Output "CSharpier failed on $filePath. The build treats formatting as an error, so fix this before building:`n$output"
+ }
+}
+catch {
+ Write-Output "tidy-cs hook error: $($_.Exception.Message)"
+}
+
+exit 0
diff --git a/.claude/settings.json b/.claude/settings.json
index 4b9217f..49ce5d4 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -7,7 +7,7 @@
"hooks": [
{
"type": "command",
- "command": "pwsh -NoProfile -NonInteractive -File \"$CLAUDE_PROJECT_DIR/.claude/hooks/format-cs.ps1\"",
+ "command": "pwsh -NoProfile -NonInteractive -File \"$CLAUDE_PROJECT_DIR/.claude/hooks/tidy-cs.ps1\"",
"timeout": 60
},
{
diff --git a/.codex/hooks.json b/.codex/hooks.json
index cb4110c..3d7122c 100644
--- a/.codex/hooks.json
+++ b/.codex/hooks.json
@@ -7,7 +7,7 @@
"hooks": [
{
"type": "command",
- "command": "pwsh -NoProfile -NonInteractive -Command \"& (Join-Path (git rev-parse --show-toplevel) '.codex/hooks/format-cs.ps1')\"",
+ "command": "pwsh -NoProfile -NonInteractive -Command \"& (Join-Path (git rev-parse --show-toplevel) '.codex/hooks/tidy-cs.ps1')\"",
"statusMessage": "Formatting changed C# files",
"timeout": 120
},
diff --git a/.codex/hooks/format-cs.ps1 b/.codex/hooks/tidy-cs.ps1
similarity index 69%
rename from .codex/hooks/format-cs.ps1
rename to .codex/hooks/tidy-cs.ps1
index d5524e1..17570e0 100644
--- a/.codex/hooks/format-cs.ps1
+++ b/.codex/hooks/tidy-cs.ps1
@@ -1,13 +1,17 @@
-# Codex PostToolUse hook: format the C# files an edit just touched, against .editorconfig.
+# Codex PostToolUse hook: format the C# files an edit just touched, with CSharpier.
#
-# The formatting logic itself lives in scripts/format-cs.ps1, which Claude Code's hook runs too.
-# This file is only the hook wiring.
+# The logic itself lives in scripts/tidy-cs.ps1, which Claude Code's hook runs too. This file is only
+# the hook wiring.
#
# Why it does not read a path out of the payload: for a file edit Codex reports tool_name "apply_patch" and
# puts the patch text in tool_input.command, not a file path. The script with no arguments formats
# every .cs file git reports as changed, which covers the edit that just happened and costs nothing when
# there is none.
#
+# Formatting only, which is the default scope. Style and member ordering are build errors and
+# scripts/preflight.ps1 runs `-Scope all` before a commit; neither belongs on the critical path of
+# every edit.
+#
# Contract (https://learn.chatgpt.com/docs/hooks): exit 0 and write the response JSON to stdout. Exit code 2
# would block the operation - this hook never does that, because a formatter problem must not stop an edit.
@@ -41,25 +45,25 @@ try {
$repositoryRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
}
- $script = Join-Path $repositoryRoot 'scripts/format-cs.ps1'
+ $script = Join-Path $repositoryRoot 'scripts/tidy-cs.ps1'
if (-not (Test-Path -LiteralPath $script)) {
- Write-HookResult -AdditionalContext "format-cs hook: scripts/format-cs.ps1 not found at $script"
+ Write-HookResult -AdditionalContext "tidy-cs hook: scripts/tidy-cs.ps1 not found at $script"
exit 0
}
$output = & pwsh -NoProfile -NonInteractive -File $script 2>&1 | Out-String
# Quiet on success: the agent does not need to be told that nothing needed formatting. A failure is
- # reported, because it means the next build breaks on a style rule.
+ # reported, because it means the next build breaks on a formatting rule.
if ($LASTEXITCODE -ne 0) {
- Write-HookResult -AdditionalContext "dotnet format failed. The build treats style rules as errors, so fix this before building:`n$output"
+ Write-HookResult -AdditionalContext "CSharpier failed. The build treats formatting as an error, so fix this before building:`n$output"
}
else {
Write-HookResult
}
}
catch {
- Write-HookResult -AdditionalContext "format-cs hook error: $($_.Exception.Message)"
+ Write-HookResult -AdditionalContext "tidy-cs hook error: $($_.Exception.Message)"
}
exit 0
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index db0b903..a47c819 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -8,6 +8,20 @@
"docfx"
],
"rollForward": false
+ },
+ "csharpier": {
+ "version": "1.3.0",
+ "commands": [
+ "csharpier"
+ ],
+ "rollForward": false
+ },
+ "jetbrains.resharper.globaltools": {
+ "version": "2026.2.1",
+ "commands": [
+ "jb"
+ ],
+ "rollForward": false
}
}
}
diff --git a/.csharpierignore b/.csharpierignore
new file mode 100644
index 0000000..048e2fe
--- /dev/null
+++ b/.csharpierignore
@@ -0,0 +1,18 @@
+# CSharpier formats C# in this repository, and nothing else.
+#
+# Since version 1.0 it can also format XML, which here means the .csproj, .props, .targets, .slnx and
+# .config files. Those are hand-maintained: they carry long explanatory comments, deliberate blank lines
+# between the property groups, and one item per line. CSharpier's XML formatter reflows all of that -
+# it splits short attribute lists across four lines each and re-indents elements without re-indenting
+# the comment bodies inside them. .editorconfig already fixes their indentation, which is all they need.
+#
+# Build output and generated documentation are not listed: CSharpier honours .gitignore, and obj, bin,
+# docs/api and docs/_site are all in there already.
+
+*.csproj
+*.props
+*.targets
+*.slnx
+*.config
+*.xml
+*.DotSettings
diff --git a/.editorconfig b/.editorconfig
index 642ed7f..8fbb815 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,47 +1,161 @@
-root = true
+root = true
+
+# ======================================================================================================
+# Every file
+#
+# Line endings are deliberately NOT set here. .gitattributes owns them: it normalizes to LF in the
+# repository and checks each file out with the ending that file type needs. An end_of_line here would
+# fight that on Windows.
+# ======================================================================================================
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{csproj,props,targets,DotSettings}]
+indent_style = tab
+tab_width = 4
+
+[*.{slnx,config,xml,json,yml,yaml}]
+indent_size = 2
+
+# Two trailing spaces are a hard line break in Markdown, so they must survive.
+[*.md]
+trim_trailing_whitespace = false
+
+# ======================================================================================================
+# C#
+#
+# Three tools share this file, and each one owns exactly one concern:
+#
+# CSharpier whitespace, line breaks, wrapping. Reads max_line_length and indent_size.
+# Roslyn analyzers code style (the settings below). Fixed by `dotnet format style`.
+# NewStyleCop.Analyzers member ordering (checking only). Fixed by ReSharper, see the ordering
+# section further down.
+#
+# Run scripts/tidy-cs.ps1 to apply all three.
+# ======================================================================================================
[*.cs]
+
+#### Layout ####
+
+indent_size = 4
+tab_width = 4
+
+# CSharpier reads this as its print width. It is the ONLY place the line width is configured.
max_line_length = 120
-charset = utf-8
-csharp_using_directive_placement = outside_namespace:error
-csharp_prefer_simple_using_statement = true:suggestion
-csharp_prefer_braces = true:error
-csharp_style_namespace_declarations = file_scoped:error
-csharp_style_prefer_method_group_conversion = true:suggestion
-csharp_style_prefer_top_level_statements = true:suggestion
-csharp_style_prefer_primary_constructors = true:error
-csharp_prefer_system_threading_lock = false:silent
-csharp_style_prefer_simple_property_accessors = true:error
-csharp_style_expression_bodied_methods = true:error
+#### Types: C# keywords, never BCL type names ####
+
+# `string`, `int`, `bool` - not `String`, `Int32`, `Boolean`. Both settings are IDE0049.
+dotnet_style_predefined_type_for_locals_parameters_members = true:error
+dotnet_style_predefined_type_for_member_access = true:error
+
+# `var` wherever the type is obvious from the right-hand side.
+csharp_style_var_for_built_in_types = true:error
+csharp_style_var_when_type_is_apparent = true:error
+
+#### Member access: always `this.` ####
+
+# Instance members are always read and written through `this.`, and fields are never `_camelCase`.
+#
+# A primary constructor parameter is assigned to a `private readonly` backing field and read through
+# `this.field`, never used directly in a member body. Nothing below enforces that - a parameter is not an
+# instance member, so these four rules cannot see it - but a captured parameter compiles to a field with no
+# `readonly`, and using one directly would silently drop the guarantee that it cannot be reassigned.
+dotnet_style_qualification_for_event = true:error
+dotnet_style_qualification_for_field = true:error
+dotnet_style_qualification_for_method = true:error
+dotnet_style_qualification_for_property = true:error
+
+#### Expression-bodied members ####
+
+# Use `=>` wherever a member is a single expression.
+csharp_style_expression_bodied_accessors = true:error
csharp_style_expression_bodied_constructors = true:error
-csharp_style_expression_bodied_operators = true:error
-csharp_style_expression_bodied_properties = true:error
csharp_style_expression_bodied_indexers = true:error
-csharp_style_expression_bodied_accessors = true:error
csharp_style_expression_bodied_lambdas = true:error
csharp_style_expression_bodied_local_functions = true:error
-csharp_style_throw_expression = true:error
-csharp_style_prefer_null_check_over_type_check = true:error
+csharp_style_expression_bodied_methods = true:error
+csharp_style_expression_bodied_operators = true:error
+csharp_style_expression_bodied_properties = true:error
+csharp_style_prefer_simple_property_accessors = true:error
+
+#### Namespaces and using directives ####
+
+# An unused using is a build error. This needs the documentation file that Directory.Build.props turns
+# on for every project - without one, IDE0005 is simply never reported at build time.
+dotnet_diagnostic.IDE0005.severity = error
+
+csharp_style_namespace_declarations = file_scoped:error
+csharp_using_directive_placement = outside_namespace:error
+dotnet_style_namespace_match_folder = true:error
+
+#### Declarations ####
+
+csharp_prefer_braces = true:error
+csharp_style_prefer_primary_constructors = true:error
+csharp_style_prefer_top_level_statements = true:suggestion
+dotnet_style_prefer_auto_properties = true:error
+
+#### Modern language features ####
+
csharp_prefer_simple_default_expression = true:error
-csharp_style_prefer_local_over_anonymous_function = true:suggestion
-csharp_style_prefer_index_operator = true:error
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_prefer_system_threading_lock = false:silent
+# `new()` wherever the compiler can infer the type. IDE0090 covers only the case where the type is
+# written next to it - `EnumerableReader r = new EnumerableReader(...)` - which the `var` rule above
+# makes impossible anyway, so on its own it has almost nothing to fire on here.
+#
+# RCS1250 covers what IDE0090 does not: `return new T(...)`, an argument, and an assignment to an
+# already-typed target. Not everything - it stays quiet on a `new` nested inside another `new` that is
+# itself an argument, as in EntityMaterializerFactory's propertyBindings.Add(...). Rider flags those;
+# the build does not, so they are on the author.
csharp_style_implicit_object_creation_when_type_is_apparent = true:error
+roslynator_object_creation_type_style = implicit
+dotnet_diagnostic.RCS1250.severity = error
+csharp_style_inlined_variable_declaration = true:error
csharp_style_prefer_implicitly_typed_lambda_expression = true:error
-csharp_style_prefer_unbound_generic_type_in_nameof = true:error
+csharp_style_prefer_index_operator = true:error
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_method_group_conversion = true:suggestion
csharp_style_prefer_not_pattern = true:error
-csharp_style_var_for_built_in_types = true:error
-csharp_style_var_when_type_is_apparent = true:error
csharp_style_prefer_range_operator = true:error
csharp_style_prefer_tuple_swap = true:error
+csharp_style_prefer_unbound_generic_type_in_nameof = true:error
csharp_style_prefer_utf8_string_literals = true:error
-csharp_style_inlined_variable_declaration = true:error
+csharp_style_throw_expression = true:error
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_prefer_collection_expression = when_types_loosely_match:error
+dotnet_style_prefer_compound_assignment = true:error
+dotnet_style_prefer_simplified_interpolation = true:error
+
+#### Null handling ####
+
+csharp_style_prefer_null_check_over_type_check = true:error
+dotnet_style_coalesce_expression = true:error
+dotnet_style_null_propagation = true:error
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error
+
+#### Expressions ####
+
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:error
csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
-dotnet_analyzer_diagnostic.category-roslynator.severity = error
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:error
+dotnet_style_prefer_inferred_tuple_names = true:error
+dotnet_style_prefer_simplified_boolean_expressions = true:error
-#### Naming styles ####
+#### Naming ####
# Naming rules
@@ -49,14 +163,9 @@ dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
-dotnet_naming_style.pascal_with_underscores.capitalization = pascal_case
-dotnet_naming_style.pascal_with_underscores.required_prefix =
-dotnet_naming_style.pascal_with_underscores.required_suffix =
-dotnet_naming_style.pascal_with_underscores.word_separator = _
-
+dotnet_naming_rule.types_should_be_pascal_with_underscores.severity = warning
dotnet_naming_rule.types_should_be_pascal_with_underscores.symbols = types
dotnet_naming_rule.types_should_be_pascal_with_underscores.style = pascal_with_underscores
-dotnet_naming_rule.types_should_be_pascal_with_underscores.severity = warning
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
@@ -66,73 +175,154 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
-dotnet_naming_symbols.interface.required_modifiers =
+dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
-dotnet_naming_symbols.types.required_modifiers =
+dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
-dotnet_naming_symbols.non_field_members.required_modifiers =
+dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
-dotnet_naming_style.begins_with_i.required_suffix =
-dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
-dotnet_naming_style.pascal_case.required_prefix =
-dotnet_naming_style.pascal_case.required_suffix =
-dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
-dotnet_style_operator_placement_when_wrapping = beginning_of_line
-tab_width = 4
-indent_size = 4
-dotnet_style_coalesce_expression = true:error
-dotnet_style_null_propagation = true:error
-dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error
-dotnet_style_prefer_auto_properties = true:error
-dotnet_style_object_initializer = true:suggestion
-dotnet_style_collection_initializer = true:suggestion
-dotnet_style_prefer_simplified_boolean_expressions = true:error
-dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
-dotnet_style_prefer_conditional_expression_over_return = true:suggestion
-dotnet_style_explicit_tuple_names = true:suggestion
-dotnet_style_prefer_inferred_tuple_names = true:error
-dotnet_style_prefer_inferred_anonymous_type_member_names = true:error
-dotnet_style_prefer_compound_assignment = true:error
-dotnet_style_prefer_simplified_interpolation = true:error
-dotnet_style_prefer_collection_expression = when_types_loosely_match:error
-dotnet_style_namespace_match_folder = true:error
-dotnet_style_predefined_type_for_locals_parameters_members = false:silent
-dotnet_style_predefined_type_for_member_access = false:silent
+dotnet_naming_style.pascal_with_underscores.required_prefix =
+dotnet_naming_style.pascal_with_underscores.required_suffix =
+dotnet_naming_style.pascal_with_underscores.word_separator = _
+dotnet_naming_style.pascal_with_underscores.capitalization = pascal_case
+
+#### Formatting is CSharpier's job ####
-# RCS1037: Remove trailing white-space
+# IDE0055 is "fix formatting" - every whitespace rule Roslyn has, under one ID. A number of its options
+# disagree with CSharpier, and CSharpier is the tool that owns whitespace here, so this stays off.
+# CSharpier's own documentation recommends exactly this.
+dotnet_diagnostic.IDE0055.severity = none
+
+# RCS1037: Remove trailing white-space. Same reason - CSharpier removes it, and trim_trailing_whitespace
+# in the [*] section above tells the editor to.
dotnet_diagnostic.RCS1037.severity = none
-# RCS1227: Validate arguments correctly
-dotnet_diagnostic.RCS1227.severity = none
+#### Member ordering: NewStyleCop.Analyzers ####
+
+# StyleCop is here for ONE job: reporting types and members that are in the wrong order. Everything else
+# it does is either CSharpier's concern or already covered by the Roslyn, Roslynator and Sonar rules in
+# this file, so every category is switched off first and only the rules we want are switched back on.
+# Adding a StyleCop rule to this repository means adding it explicitly below.
+#
+# StyleCop can only REPORT a wrong order - its ordering code fix is disabled upstream. ReSharper does the
+# fixing: in Rider through Code Cleanup, and on the command line through scripts/tidy-cs.ps1. So the
+# order is defined twice, and the two definitions have to stay in step:
+# stylecop.json -> what is checked
+# DbConnectionPlus.slnx.DotSettings -> what is applied
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.DocumentationRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.LayoutRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.MaintainabilityRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.NamingRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.OrderingRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.ReadabilityRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.SpacingRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.SpecialRules.severity = none
+
+# SA1208, SA1209, SA1210, SA1211, SA1217: the order of using directives.
+#
+# CSharpier already sorts using directives, so these five do not fix anything - they CHECK that what
+# CSharpier produced is what we want, and catch a file that reached the repository without it. All five
+# report zero violations across the 283 files, which is what says CSharpier and StyleCop agree here.
+#
+# SA1208 is also what makes `systemUsingDirectivesFirst` in stylecop.json mean something. With the rule
+# off, that setting was read by nothing.
+#
+# SA1216 (using static placed after the regular usings) is deliberately ABSENT: it is the one CSharpier
+# contradicts. CSharpier sorts by namespace and ignores the `static` keyword, so it writes
+#
+# global using AwesomeAssertions;
+# global using static AwesomeAssertions.FluentActions;
+# global using Microsoft.Data.SqlClient;
+#
+# where SA1216 wants both `using static` lines grouped at the end. Enabling it makes the build
+# unsatisfiable, because CSharpier runs last and always has the final say - verified by sorting the file
+# StyleCop's way and watching CSharpier put it straight back.
+#
+# SA1200 is absent for a different reason: `csharp_using_directive_placement` above already requires
+# usings outside the namespace, and two rules for one decision is how they end up disagreeing.
+dotnet_diagnostic.SA1208.severity = error
+dotnet_diagnostic.SA1209.severity = error
+dotnet_diagnostic.SA1210.severity = error
+dotnet_diagnostic.SA1211.severity = error
+dotnet_diagnostic.SA1217.severity = error
+
+# SA1201: Elements should appear in the correct order (by kind).
+dotnet_diagnostic.SA1201.severity = error
+
+# SA1202: Elements should be ordered by access.
+#
+# Explicit interface implementations need a word, because the checker and the fixer classify them
+# differently. ReSharper ranks one below private, since in C# it carries no access modifier, so left
+# alone it lands at the bottom of its kind group.
+#
+# StyleCop disagrees, but NOT uniformly - which is why the file layout has one entry per kind rather
+# than one shared entry:
+#
+# properties, indexers, methods counted as public, so they must come FIRST in their group.
+# The layout has an "Explicit interface " entry for each.
+# events counted as private, so ReSharper's default placement is already
+# right. Giving events an entry actively breaks the build - it puts
+# the explicit event ahead of a public one, which is SA1202.
+#
+# The entries match on ImplementsInterface AND Access Is="Private", and both halves matter.
+# ImplementsInterface on its own also matches IMPLICIT implementations, which drags Equals(T) away
+# from Equals(object) and trips Sonar's S4136. An implicit implementation has to be public, so the
+# access test is what narrows each entry to the explicit ones.
+#
+# One shared entry across the kinds would not work either: it would sit an explicit property next to
+# an explicit method and break SA1201's kind order.
+dotnet_diagnostic.SA1202.severity = error
+
+# SA1203: Constants should appear before fields.
+dotnet_diagnostic.SA1203.severity = error
+
+# SA1204: Static elements should appear before instance elements.
+dotnet_diagnostic.SA1204.severity = error
+
+# SA1214: Readonly fields should appear before non-readonly fields.
+dotnet_diagnostic.SA1214.severity = error
+
+# SA1309: Field names should not begin with an underscore. Nothing else enforces this, and it is the
+# other half of the "always `this.`" decision - see the member access section above.
+dotnet_diagnostic.SA1309.severity = error
+
+#### Roslynator ####
+
+dotnet_analyzer_diagnostic.category-roslynator.severity = error
# RCS1124: Inline local variable
dotnet_diagnostic.RCS1124.severity = suggestion
-# IDE0290: Use primary constructor
-dotnet_diagnostic.IDE0290.severity = none
-
-# CA2100: Review SQL queries for security vulnerabilities
-dotnet_diagnostic.CA2100.severity = none
-
# RCS1222: Merge preprocessor directives
dotnet_diagnostic.RCS1222.severity = none
+# RCS1227: Validate arguments correctly
+dotnet_diagnostic.RCS1227.severity = none
+
# RCS1237: Use bit shift operator
dotnet_diagnostic.RCS1237.severity = none
-# xUnit1051: Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken
-dotnet_diagnostic.xUnit1051.severity = none
+#### .NET analyzers ####
+
+# CA2100: Review SQL queries for security vulnerabilities
+# Running SQL the caller wrote is what this library does. Same reason S2077 is off below.
+dotnet_diagnostic.CA2100.severity = none
#### SonarAnalyzer.CSharp ####
@@ -142,6 +332,16 @@ dotnet_diagnostic.xUnit1051.severity = none
# The two hits are deliberate pseudo-code explaining the expression trees the materializer factories emit.
dotnet_diagnostic.S125.severity = none
+# S2077: Formatting SQL queries is security-sensitive
+# The temporary table builders emit DDL, where a table or column name can never be a bound parameter. Same
+# reason CA2100 is off above; the identifiers are quoted per adapter, not interpolated user input.
+dotnet_diagnostic.S2077.severity = none
+
+# S2325: Methods and properties that don't access instance data should be static
+# CA1822 already covers this and correctly skips public members. S2325 does not: both hits are shipped public
+# API listed in PublicAPI.Shipped.txt, where going static is a breaking change.
+dotnet_diagnostic.S2325.severity = none
+
# S3011: Reflection should not be used to increase accessibility of classes, methods, or fields
# Binding non-public constructors and members is what an entity materializer does; the sites are documented.
dotnet_diagnostic.S3011.severity = none
@@ -158,35 +358,68 @@ dotnet_diagnostic.S3881.severity = none
# The generic throw helpers name the CALLER's parameter, which by design is not in their own signature.
dotnet_diagnostic.S3928.severity = none
-# S2077: Formatting SQL queries is security-sensitive
-# The temporary table builders emit DDL, where a table or column name can never be a bound parameter. Same
-# reason CA2100 is off above; the identifiers are quoted per adapter, not interpolated user input.
-dotnet_diagnostic.S2077.severity = none
-
-# S2325: Methods and properties that don't access instance data should be static
-# CA1822 already covers this and correctly skips public members. S2325 does not: both hits are shipped public
-# API listed in PublicAPI.Shipped.txt, where going static is a breaking change.
-dotnet_diagnostic.S2325.severity = none
-
# S4456: Parameter validation in yielding methods should be wrapped
# The public Query overloads validate their arguments inside the iterator, so the exception surfaces on the
# first MoveNext instead of at the call. Fixing it means splitting four public methods.
dotnet_diagnostic.S4456.severity = none
-[*.ps1]
-end_of_line = crlf
+#### xUnit ####
+
+# xUnit1051: Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken
+dotnet_diagnostic.xUnit1051.severity = none
+
+# ======================================================================================================
+# Tests and benchmarks
+#
+# Held to the same style, formatting and ordering as the shipping libraries. What they are NOT held to is
+# XML documentation, and a handful of Sonar rules that only make sense for library code.
+# ======================================================================================================
[{tests,benchmarks}/**/*.cs]
+# CS1591: Missing XML comment for publicly visible type or member
+# CS1574: XML comment has a cref attribute that could not be resolved
+#
+# Every project generates a documentation file, because IDE0005 does not work without one - see the
+# comment in Directory.Build.props. That switches these two compiler diagnostics on everywhere as a side
+# effect, and with TreatWarningsAsErrors they are 3202 errors here. Nothing consumes the tests or the
+# benchmarks as an API and they use plain `//` comments by design, so both are off for them. In src/ they
+# stay errors, which is what actually keeps the public API documented.
+dotnet_diagnostic.CS1574.severity = none
+dotnet_diagnostic.CS1591.severity = none
+
+# S1144: Unused private types or members should be removed
+dotnet_diagnostic.S1144.severity = none
+
+# S1172: Unused method parameters should be removed
+dotnet_diagnostic.S1172.severity = none
+
+# S2344: Enumeration type names should not have "Flags" or "Enum" suffixes
dotnet_diagnostic.S2344.severity = none
+
+# S2696: Instance members should not write to "static" fields
+dotnet_diagnostic.S2696.severity = none
+
+# S2925: "Thread.Sleep" should not be used in tests
+dotnet_diagnostic.S2925.severity = none
+
+# S3010: Static fields should not be updated in constructors
dotnet_diagnostic.S3010.severity = none
-dotnet_diagnostic.S4144.severity = none
-dotnet_diagnostic.S3459.severity = none
+
+# S3453: Classes should not have only "private" constructors
dotnet_diagnostic.S3453.severity = none
-dotnet_diagnostic.S1144.severity = none
-dotnet_diagnostic.S1172.severity = none
+
+# S3459: Unassigned members should be removed
+dotnet_diagnostic.S3459.severity = none
+
+# S3963: "static" fields should be initialized inline
dotnet_diagnostic.S3963.severity = none
-dotnet_diagnostic.S6562.severity = none
-dotnet_diagnostic.S2925.severity = none
+
+# S4144: Methods should not have identical implementations
+dotnet_diagnostic.S4144.severity = none
+
+# S5034: "ValueTask" should be consumed correctly
dotnet_diagnostic.S5034.severity = none
-dotnet_diagnostic.S2696.severity = none
+
+# S6562: Always set the "DateTimeKind" when creating new "DateTime" instances
+dotnet_diagnostic.S6562.severity = none
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 0000000..36e29a5
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,39 @@
+# Commits that rewrote lines across most of the repository without changing what the code does.
+# `git blame` skips them, so it points at whoever wrote the logic rather than at the tool that
+# reformatted it.
+#
+# GitHub reads this file automatically. Locally you have to opt in, once per clone:
+#
+# git config blame.ignoreRevsFile .git-blame-ignore-revs
+#
+# Add a commit here when its effect on SOURCE files is entirely tool output. A commit that also carries
+# hand-written configuration or documentation still qualifies - blame on the code is what this file is
+# protecting - but say so in its note.
+#
+# The SHAs below are load-bearing, and git does NOT warn about one it cannot resolve: it skips the entry,
+# and blame quietly goes back to pointing at the tool. "Rebase and merge" and "Squash and merge" rewrite
+# every SHA on a branch, so merge a branch that adds an entry here with a MERGE COMMIT. CI's lint job
+# fails if any revision below stops resolving, which is what turns that mistake into something visible.
+
+# style: use C# keywords instead of BCL type names
+# dotnet format style --diagnostics IDE0049. 207 files.
+89bc6d82d1dcb8033083af45553d32d06cbc6fd8
+
+# style: convert eligible constructors to primary constructors
+# dotnet format style --diagnostics IDE0290, six sites. MIXED: also removes 23 stale
+# "Initializes a new instance" doc lines the fixer leaves behind.
+3c4793070f06013f6bfe28cf1b921b464e845e45
+
+# style: reformat every C# file with CSharpier
+# csharpier format . - the first run, after which nobody places a line break by hand.
+f80b2f75fa4a8331c1b63aef1686144560d1e904
+
+# style: reorder type members into StyleCop order
+# jb cleanupcode --profile=ReorderMembers, then csharpier. Fields moved to the top of each type.
+48c97d6088a601ee461bf0546150f4672b47f471
+
+# style: satisfy the gates the previous commit turned on
+# dotnet format (IDE0005, RCS1250) and jb cleanupcode. MIXED: the keyword conversions under
+# tests/package-consumption/ and the MySql primary constructor were applied by hand, because no tool
+# in the pipeline reaches them.
+70f9ea67d055d9932c1ff9f5bf3204af9e1c78c4
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index d59bfe5..96162c2 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -15,4 +15,6 @@
- [ ] Public API changes are declared in the affected `PublicAPI.Unshipped.txt` (`scripts/update-public-api.ps1`).
- [ ] XML docs and `README.md` updated for public API changes.
- [ ] `CHANGELOG.md` updated, and the version in `src/Directory.Build.props` bumped if this release-bound change needs it.
-- [ ] Code formatted (`pwsh -File scripts/format-cs.ps1`).
+- [ ] Style, formatting and member ordering applied (`pwsh -File scripts/tidy-cs.ps1 -Scope all`, which
+ `preflight.ps1` also runs).
+- [ ] Branch name follows [Conventional Branch](https://conventionalbranch.org/): `/issue--`.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3a91b01..da04ec6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -37,23 +37,80 @@ env:
TESTINGPLATFORM_TELEMETRY_OPTOUT: true
jobs:
- # Enforce the formatting the repo ships tooling for. A style rule that is not enforced in CI drifts with
- # the first external pull request.
+ # Enforce the style, formatting and member ordering the repo ships tooling for. A rule that is not
+ # enforced in CI drifts with the first external pull request.
#
- # Whitespace only: the semantic style and analyzer rules are already enforced by the build itself
- # (EnforceCodeStyleInBuild + TreatWarningsAsErrors + Roslynator at error) in the verify job below. What
- # that does NOT cover is tests/ and benchmarks/, which set neither property - so for those projects this
- # job is the only formatting gate there is.
+ # This runs scripts/tidy-cs.ps1, the same entry point a developer and both AI agents use, so CI cannot
+ # disagree with what the tooling produces locally. -Check -Scope all tidies the checkout for real and
+ # then asks whether anything changed: ReSharper has no check mode, and neither it nor CSharpier is
+ # idempotent alone, so comparing before against after is the only honest question. Writing to the
+ # checkout is harmless here - it is thrown away at the end of the job.
+ #
+ # All three are also build errors in the verify job below, for every project - style and ordering through
+ # the analyzers, formatting through CSharpier.MsBuild. This job still earns its place: it is much faster to
+ # a red X, and it prints the exact diff that would fix things rather than only naming the file. It is also
+ # the only check that can see a MISPLACED member the analyzers accept, because alphabetical order within a
+ # group is a ReSharper notion that StyleCop does not model.
+ #
+ # It also guards .git-blame-ignore-revs, which is repository hygiene of the same kind: a rule nothing
+ # checks stops being true. See the step for why that file cannot report its own breakage.
lint:
name: Lint
runs-on: ubuntu-latest
- timeout-minutes: 15
+ timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
+ # Full history, for the .git-blame-ignore-revs check below. At the default depth of 1 none of the
+ # revisions it names would resolve and every run would fail. Nothing else in this job needs it.
+ fetch-depth: 0
+
+ # .git-blame-ignore-revs names commits by raw SHA, and git does NOT complain about one it cannot
+ # resolve - it silently skips the entry. The file then still looks right while doing nothing, and blame
+ # goes back to pointing at the tool that reformatted a line instead of at whoever wrote it.
+ #
+ # A rebase-merge or a squash-merge rewrites every SHA on a branch, which is exactly how that happens,
+ # and it happens quietly, months after the click. This step is the only thing that makes it visible.
+ #
+ # It runs before the .NET setup on purpose: it needs nothing but the checkout, so a broken file fails in
+ # seconds instead of after the three-minute tidy run below.
+ - name: Verify .git-blame-ignore-revs
+ run: |
+ if [ ! -f .git-blame-ignore-revs ]; then
+ echo "No .git-blame-ignore-revs - nothing to check."
+ exit 0
+ fi
+
+ status=0
+ count=0
+ while read -r revision _ || [ -n "${revision}" ]; do
+ revision="${revision%$'\r'}"
+ case "${revision}" in ''|\#*) continue ;; esac
+ count=$((count + 1))
+
+ if ! git rev-parse --verify --quiet "${revision}^{commit}" > /dev/null; then
+ echo "::error file=.git-blame-ignore-revs::${revision} is not a commit in this repository. If the branch that introduced it was rebased or squash-merged, replace it with the SHA it became."
+ status=1
+ elif ! git merge-base --is-ancestor "${revision}" HEAD; then
+ echo "::error file=.git-blame-ignore-revs::${revision} is a commit, but not an ancestor of HEAD, so blame can never reach it."
+ status=1
+ fi
+ done < .git-blame-ignore-revs
+
+ if [ "${count}" -eq 0 ]; then
+ echo "::error file=.git-blame-ignore-revs::The file lists no revisions. Delete it if it is no longer wanted, rather than leaving an empty one that looks like it is doing something."
+ exit 1
+ fi
+
+ if [ "${status}" -ne 0 ]; then
+ echo "Checked ${count} revision(s); see the errors above."
+ exit 1
+ fi
+
+ echo "All ${count} revision(s) resolve and are reachable."
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
@@ -74,8 +131,16 @@ jobs:
- name: Restore
run: dotnet restore ${{ env.SOLUTION }} --configfile ${{ env.NUGET_CONFIG }}
- - name: Verify C# formatting (dotnet format)
- run: dotnet format whitespace ${{ env.SOLUTION }} --no-restore --verify-no-changes
+ - name: Restore .NET tools
+ run: dotnet tool restore
+
+ - name: Verify style, formatting and member ordering
+ run: pwsh -NoProfile -NonInteractive -File scripts/tidy-cs.ps1 -Scope all -Check
+
+ # The check above only says that something is untidy. This says what.
+ - name: Show the changes that would fix it
+ if: failure()
+ run: git --no-pager diff --stat && git --no-pager diff
# Build the solution, run both test suites against real databases, and hand the coverage to Codecov.
#
diff --git a/AGENTS.md b/AGENTS.md
index c2d8c16..3583628 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,15 +1,14 @@
# AGENTS.md
-Guidance for any AI coding agent (Claude Code, Codex, …) working in this repository. This file is the
-canonical source; `CLAUDE.md` is a pointer to it plus Claude Code-specific wiring.
+Guidance for any AI coding agent (Claude Code, Codex, …) working in this repository. This file is the canonical
+source; `CLAUDE.md` is a pointer to it plus Claude Code-specific wiring.
## What this is
**DbConnectionPlus** — a lightweight .NET ORM / extension library for `System.Data.Common.DbConnection`. It adds
type-safe, high-performance helpers (`Query`, `InsertEntity`, `UpdateEntities`, temporary tables, …) as extension
-methods on `DbConnection`, with per-database dialect support supplied by pluggable adapters.
-
-Published to NuGet as `DbConnectionPlus`; assemblies are named `RentADeveloper.DbConnectionPlus.*`.
+methods on `DbConnection`, with per-database dialect support from pluggable adapters. Published to NuGet as
+`DbConnectionPlus`; assemblies are named `RentADeveloper.DbConnectionPlus.*`.
## Layout
@@ -17,284 +16,181 @@ Published to NuGet as `DbConnectionPlus`; assemblies are named `RentADeveloper.D
|---|---|
| `src/DbConnectionPlus` | Core library. Root namespace `RentADeveloper.DbConnectionPlus`. |
| `src/DbConnectionPlus.DatabaseAdapters.{MySql,Oracle,PostgreSql,Sqlite,SqlServer}` | One adapter project per database system. |
-| `tests/DbConnectionPlus.UnitTests` | xUnit v3 unit tests. No database required. |
-| `tests/DbConnectionPlus.IntegrationTests` | xUnit v3 integration tests. Requires Docker (see below). |
-| `tests/package-consumption/` | Console apps that consume the **packed NuGet packages** rather than project references. `AotConsumer` is published with Native AOT (`scripts/verify-package-aot.ps1`); `AllAdaptersConsumer` installs all six packages and is built by CI on the .NET 8 SDK alone. Not xUnit projects, and deliberately **not** in the solution — see [their README](tests/package-consumption/README.md). |
-| `benchmarks/DbConnectionPlus.Benchmarks` | BenchmarkDotNet suite. Everything runs on the JIT; the three entity-mapping categories **also** run as a Native AOT binary. Run it via `scripts/benchmarks.ps1`, and read [its README](benchmarks/DbConnectionPlus.Benchmarks/README.md) before adding a benchmark. |
-| `docs/` | docfx config, the site landing page + implementation plans. |
-| `.agents/` | Canonical repository skills and review checklists shared by both agent integrations. |
-| `.codex/` | Codex custom-agent metadata and PostToolUse hook wiring. |
-| `.github/workflows/` | `ci.yml` (lint → build/test/analyze → package + docs → package-consumption gates → publish → release), plus `codeql.yml` and `dependency-review.yml`. |
-| `scripts/` | Entry points **you** type: `preflight`, `verify-package-aot`, `benchmarks`, `update-public-api`, `clean-build-artifacts`, `extract-release-notes`. Also `format-cs` and `public-api-guard`, which the editor hooks run for you. |
-
-Solution file is `DbConnectionPlus.slnx` (the XML `.slnx` format, not `.sln`). **New projects must be added to it.**
-
-Build properties live in **two** `Directory.Build.props` files, not in the `.csproj` files:
-
-| File | Applies to | Carries |
-|---|---|---|
-| `Directory.Build.props` (repo root) | all nine solution projects | `Authors`, `Company`, `Copyright`, `ImplicitUsings`, `LangVersion`, `Nullable`, `TieredCompilation`, and the ErrorProne.NET + Roslynator analyzers |
-| `src/Directory.Build.props` | the six shipping projects | ``, `TargetFrameworks=net8.0;net10.0`, `IsAotCompatible`, `AnalysisLevel=latest-all`, `TreatWarningsAsErrors`, the AOT and public-API analyzers, the NuGet package metadata, and the files every package carries |
-
-MSBuild uses the **nearest** `Directory.Build.props` and stops, so `src/Directory.Build.props` imports the root
-one explicitly — remove that import and the six shipping projects silently lose authorship, nullability and the
-style analyzers. The split exists because `tests/` and `benchmarks/` set neither `TreatWarningsAsErrors` nor
-`AnalysisLevel` and would newly break under them. (The benchmarks are `net10.0` — BenchmarkDotNet's
-`NativeAotToolchain.Net10_0` publishes that TFM. The **unit tests multi-target `net8.0;net10.0`**, because the
-two builds of the shipping libraries are not the same code — `net8.0` carries an `IL3050` suppression
-`net10.0` does not. The integration tests stay `net8.0`: they are bound by four database containers rather
-than by the runtime, and doubling a ten-minute suite buys nothing the unit tests do not already cover on both.)
-
-`tests/package-consumption/` sits **outside** all of this on purpose: it carries its own empty
-`Directory.Build.props`/`.targets` that stop MSBuild's upward search, so those projects receive the library
-only from the packed packages. Do not "fix" that by deleting the empty files.
-
-What stays in a `.csproj` is per-project identity — `AssemblyName`, `AssemblyTitle`, `RootNamespace`,
-`PackageId`, `Description`, `PackageTags` — plus that project's own package references. A shipping `.csproj` is
-about 20 lines. The package icon is **one** file, `assets/logo-128.png`, referenced from
-`src/Directory.Build.props` for all six packages.
-
-**Two readmes, and they are not interchangeable.** `README.md` is the repository's reference documentation and what a GitHub visitor reads. `PACKAGE_README.md` is what nuget.org renders as the package
-page: a short overview for someone deciding whether to install, which links back to the long one. Both ship
-from the repo root; only `PACKAGE_README.md` goes into the packages. An API change updates `README.md`; touch
-`PACKAGE_README.md` only when the overview itself stops being true.
+| `tests/DbConnectionPlus.{Unit,Integration}Tests` | The two xUnit v3 suites. The unit tests need no database; the integration tests need Docker. |
+| `tests/package-consumption/` | Console apps that consume the **packed NuGet packages** instead of project references. Deliberately **not** in the solution — [their README](tests/package-consumption/README.md). |
+| `benchmarks/DbConnectionPlus.Benchmarks` | BenchmarkDotNet suite, run via `scripts/benchmarks.ps1`. Read [its README](benchmarks/DbConnectionPlus.Benchmarks/README.md) before adding a benchmark. |
+| `docs/` | docfx config, the site landing page and implementation plans. |
+| `.agents/`, `.codex/`, `.claude/` | Canonical skills and references, plus each tool's agent metadata and hook wiring. |
+| `.github/workflows/` | `ci.yml` (lint → build/test → package + docs → package-consumption gates → publish), `codeql.yml`, `dependency-review.yml`. |
+| `scripts/` | The commands you type: `preflight`, `verify-package-aot`, `benchmarks`, `update-public-api`, `clean-build-artifacts`, `extract-release-notes`. Plus `tidy-cs` and `public-api-guard`, which the editor hooks run for you. |
+
+The solution file is `DbConnectionPlus.slnx` (XML `.slnx`, not `.sln`). **New projects must be added to it.**
+
+Build properties live in two `Directory.Build.props` files, not in the `.csproj` files: the repo-root one for all
+nine solution projects (shared metadata, and the **style gate** — `EnforceCodeStyleInBuild` +
+`TreatWarningsAsErrors`), and `src/Directory.Build.props` for the six shipping projects (``,
+`TargetFrameworks`, `IsAotCompatible`, `AnalysisLevel=latest-all`, the AOT and public-API analyzers, the package
+metadata). The `src/` one imports the root explicitly, because MSBuild stops at the nearest file, and it is where
+`AnalysisLevel=latest-all` has to stay — CA1707 alone objects 2100 times to the test suite's
+`Method_ShouldDoSomething` naming. Target frameworks differ per project on purpose: benchmarks `net10.0`, unit
+tests `net8.0;net10.0` (the shipping libraries' two builds are not the same code), integration tests `net8.0`.
+
+`tests/package-consumption/` sits **outside** all of this on purpose. It carries its own empty
+`Directory.Build.props`/`.targets` that stop MSBuild's upward search, so those projects get the library only from
+the packed packages. Do not "fix" that by deleting the empty files.
+
+**Two readmes, and they are not interchangeable.** `README.md` is the repository's reference documentation, what a
+GitHub visitor reads, and what an API change updates. `PACKAGE_README.md` is the short overview nuget.org renders
+as the package page: it links back to the long one, it is the only one that ships inside the packages, and you
+touch it only when the overview itself stops being true.
### The adapter seam
`IDatabaseAdapter` (`src/DbConnectionPlus/DatabaseAdapters/IDatabaseAdapter.cs`) exposes `IEntityManipulator` and
-`ITemporaryTableBuilder`. Each of the five adapter projects implements all three:
+`ITemporaryTableBuilder`, and each of the five adapter projects implements all three.
-```
-{Db}DatabaseAdapter.cs {Db}EntityManipulator.cs
-{Db}TemporaryTableBuilder.cs {Db}ConfigurationExtensions.cs
-```
-
-**A change to one adapter almost always needs mirroring into the other four.** Only the integration suite catches a
-miss, and that needs Docker. Use the `adapter_parity_reviewer` Codex custom agent, walk
+**A change to one adapter almost always needs mirroring into the other four.** Only the integration suite catches
+a miss, and that needs Docker. Run the adapter-parity reviewer, walk
[its checklist](.agents/references/reviews/adapter-parity.md) over the diff, or check the other four by hand.
-Core marks internals visible to all five adapters and to the test/benchmark assemblies
-(`src/DbConnectionPlus/AssemblyAttributes.cs`).
-
## Build & test
```bash
dotnet build DbConnectionPlus.slnx -c Release
-```
-
-```bash
dotnet test --project tests/DbConnectionPlus.UnitTests/DbConnectionPlus.UnitTests.csproj
+pwsh -File scripts/preflight.ps1 # both, plus hygiene and tidying — the default loop
+pwsh -File scripts/verify-package-aot.ps1 -Pack # the Native AOT gate
```
-Both, plus the repo-hygiene checks, in one command:
-
-```bash
-pwsh -File scripts/preflight.ps1
-```
-
-The Native AOT gate — packs the six projects, publishes `tests/package-consumption/AotConsumer` natively
-**from the packages**, gates its IL diagnostics and runs the binary. It is the **only** check that can see
-silent trimming damage, because nothing is trimmed on the JIT. Needs a C++ toolchain: MSVC on Windows,
-`clang` + `zlib1g-dev` on Linux.
-
-```bash
-pwsh -File scripts/verify-package-aot.ps1 -Pack
-```
-
-Run it when you change reflection, DAM annotations, the materializers or the temp-table readers. It is not part
-of `preflight.ps1` — a pack plus a native publish takes minutes. `-Framework net8.0` checks the documented AOT
-floor, which behaves differently from the `net10.0` default; CI runs both. Drop `-Pack` to reuse the packages
-already in `artifacts/packages`.
-
-It consumes packages rather than projects because the annotations, the embedded `ILLink.Descriptors.xml` and
-the `IsTrimmable` marker all have to survive `dotnet pack` — a project-referenced version of this check would
-stay green if packing dropped every one of them.
-
-Integration tests need a running Docker daemon and nothing else — no container to start by hand, no connection
-string to configure:
-
-```bash
-dotnet test --project tests/DbConnectionPlus.IntegrationTests/DbConnectionPlus.IntegrationTests.csproj
-```
-
-[Testcontainers](https://dotnet.testcontainers.org/) owns the databases. The container definitions live in
-`tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/`, one fixture per database system, and each one
-builds its own connection string from the free host port its container was published on — which is why nothing
-collides with a locally installed server and why there is no port to agree on with CI. Containers start **lazily,
-per database system**: a run filtered to SQLite and SQL Server never starts the MySQL, Oracle or PostgreSQL
-container, and SQLite needs none at all. They are removed when the run ends, so every run starts from a clean
-server and pays for the startup — 5 to 24 seconds each, Oracle being the slow one. Details, measured timings and
-what to do when a container will not start: [the `integration-db` skill](.agents/skills/integration-db/SKILL.md).
-
-**Scope the run.** The integration suite is not part of the default verification loop — `scripts/preflight.ps1`
-is. Run it when a change can only be proven against a real database, and when you do, the default scope is
-**SQLite + SQL Server** (~90 s, versus ~600 s for all five). Add another adapter's tests **only if you changed
-that adapter's code**; run all five only for a change to `IDatabaseAdapter`, `IEntityManipulator` or
-`ITemporaryTableBuilder`, which all five implement. Rules and measured timings:
-[the `integration-db` skill](.agents/skills/integration-db/SKILL.md).
-
-## Code style — the build enforces this
-
-`TreatWarningsAsErrors=true` with `AnalysisLevel=latest-all`, `EnforceCodeStyleInBuild=true`, ErrorProne.NET, and
-Roslynator's entire category at `error`. A style slip is a build break, not a warning.
-
-- **BCL type names, PascalCase**: write `String`, `Object?`, `Int32`, `Boolean` — *not* `string`, `object`, `int`,
- `bool`. (`dotnet_style_predefined_type_for_*` is `false`.)
-- **Expression-bodied members are `error`-severity** for methods, constructors, operators, properties, indexers,
- accessors, lambdas and local functions. Use `=>` wherever a member is a single expression.
-- **File-scoped namespaces** and usings outside the namespace.
-- **Max line length** is **120**.
-- **No UTF-8 BOM.** `.editorconfig` sets `charset = utf-8` for `*.cs`. That is the whole rule: editors honour
- it when they write a file, and CI's `dotnet format whitespace --verify-no-changes` fails a BOM'd file as
- `error CHARSET`. Git cannot help — it treats a BOM as content — so that lint
- step is the gate. To fix one, run `pwsh -File scripts/format-cs.ps1 -Scope all` (or `-Scope whitespace`);
- the default `style` scope does not apply the charset fixer. It matters because a BOM read as plain UTF-8
- becomes an invisible U+FEFF on the first token, which silently breaks anchored edits.
-- **Copyright header** on every new `.cs` file:
- ```csharp
- // Copyright (c) 2026 David Liebeherr
- // Licensed under the MIT License. See LICENSE.md in the project root for more information.
- ```
-- **XML docs** on all public and most internal members of the **shipping** projects — ``, ``,
- ``, ``. Match the density of the file you are editing; `DbConnectionExtensions.QueryFirst.cs`
- sets the bar. Invalid XML docs break the docfx workflow. The benchmarks are the exception: nothing consumes
- them as an API, so they use plain `//` comments and switch `RCS1181` off for that reason.
+Search the **whole repository** when a change touches a shared symbol. A search scoped to `src/` will miss call
+sites in `tests/` and `benchmarks/`; `EntityHelper.GetEntityTypeMetadata`, for scale, is used in 18 files across
+eight projects. The Release build covers every project, so it turns a missed call site into a build error.
+
+The Native AOT gate is the **only** check that can see silent trimming damage, because nothing is trimmed on the
+JIT. Run it whenever you change reflection, DAM annotations, the materializers or the temp-table readers. It packs
+the six projects, publishes `AotConsumer` natively **from the packages**, gates its IL diagnostics and runs the
+binary. It needs a C++ toolchain and minutes, so it is not in `preflight.ps1`; `-Framework net8.0` checks the
+documented AOT floor, and CI runs that as well as the `net10.0` default.
+
+Integration tests need only a running Docker daemon; [Testcontainers](https://dotnet.testcontainers.org/) starts a
+container per database system and removes it afterwards. **Scope the run:** the default is **SQLite + SQL Server**
+(~90 s, against ~600 s for all five); add another adapter's tests **only if you changed that adapter's code**; run
+all five only for a change to `IDatabaseAdapter`, `IEntityManipulator` or `ITemporaryTableBuilder`. Commands,
+timings and troubleshooting: [the `integration-db` skill](.agents/skills/integration-db/SKILL.md).
+
+## Code style, formatting and ordering
+
+Formatting is CSharpier's, style is the Roslyn analyzers', ordering is ReSharper's and NewStyleCop's. All three are
+build errors, in `tests/` and `benchmarks/` as much as in `src/`. Details, and the three cases where a tool does
+something surprising: [the code-style reference](.agents/references/code-style.md).
+
+- **C# keywords, never BCL type names**: `string`, `object?`, `int`, `bool`, `nint`, `nuint` — not `String`,
+ `Object?`, `Int32`, `Boolean`, `IntPtr`, `UIntPtr`. ⚠️ **The build does not catch all of this.** `IDE0049`
+ ignores `nint`/`nuint` and never looks inside `nameof(...)`, and `tests/package-consumption/` has no style gate
+ at all. Apply the rule by hand in those three places — reasons in
+ [the reference](.agents/references/code-style.md#where-the-build-misses-a-bcl-type-name).
+- **Always `this.`** for instance fields, properties, methods and events; fields are never `_camelCase` (`SA1309`).
+ A **primary constructor parameter** is assigned to a `private readonly` backing field and read through
+ `this.field` — never used directly in a member body. ⚠️ **The build does not catch this.** A captured parameter
+ compiles to a field with no `readonly`, so using one directly silently drops the guarantee that the value cannot
+ be reassigned — [why it matters](.agents/references/code-style.md#primary-constructor-parameters).
+- **Member order** is StyleCop's: constants and fields at the **top** of a type, then constructors, finalizers,
+ delegates, events, enums, interfaces, properties, indexers, operators, methods, nested types; within each group
+ public before private, static before instance, readonly before mutable, then alphabetical. Write a new member
+ into the right place instead of relying on the fixer — [full order](.agents/references/code-style.md#member-order).
- **Resolve analyzer diagnostics, don't suppress.** If a suppression is unavoidable use a scoped `#pragma` with a
comment, matching the `#pragma warning disable CA1710` style in `Dynamic/DataRow.cs`.
⚠️ **An `IL2xxx` trim warning is not yours to suppress.** They are the only build-time proof that the
`[DynamicallyAccessedMembers]` chain is complete, and an incomplete chain means silently unpopulated entities
- under trimming. The library has exactly two sanctioned `IL2xxx` suppressions — `IL2060` in
- `MaterializerFactoryHelper` and `IL2065` in `ValueTupleMaterializerFactory` — each narrow, justified in place
- and backed by a test; a third one is a finding. Never suppress one at a public API.
-- Nullable and `ImplicitUsings` are enabled; common namespaces come from `GlobalUsings.cs`.
-
-**Format edited `.cs` files before building** — it is much cheaper than discovering a style break in the build:
+ under trimming. Exactly two are sanctioned, both narrow and backed by a test: `IL2060` in
+ `MaterializerFactoryHelper`, `IL2065` in `ValueTupleMaterializerFactory`. A third is a finding, and never
+ suppress one at a public API.
+- **No C# 14 extension member syntax** until docfx [supports it](https://github.com/dotnet/docfx/issues/10808).
+- **No UTF-8 BOM** (`.editorconfig` sets `charset = utf-8` for `*.cs`). A BOM read as plain UTF-8 becomes an
+ invisible U+FEFF on the first token, which silently breaks anchored edits.
+- **Copyright header** on every new `.cs` file — copy the `// Copyright (c) 2026 David Liebeherr` and
+ `// Licensed under the MIT License. …` pair from an existing file.
+- **XML docs** on all public and most internal members of the **shipping** projects; invalid ones break the docfx
+ workflow, and benchmarks are exempt. [Details](.agents/references/code-style.md#xml-documentation-comments).
+- Primary constructors, `=>` for single-expression members, file-scoped namespaces, the 120-column limit, nullable
+ and `ImplicitUsings` — [the build fully enforces these](.agents/references/code-style.md#rules-the-build-fully-enforces).
+
+One entry point applies all of it, in three scopes:
```bash
-pwsh -File scripts/format-cs.ps1
+pwsh -File scripts/tidy-cs.ps1 # ~1s formatting, on the files git reports as changed
+pwsh -File scripts/tidy-cs.ps1 -Scope style # ~15s + the code-style fixers
+pwsh -File scripts/tidy-cs.ps1 -Scope all # ~3min + member ordering, whole solution
```
-With no arguments that formats every `.cs` file git reports as changed; pass paths to format specific files.
-Claude Code and Codex both run it automatically on every edit via a PostToolUse hook, so under either agent it
-is already done — see [.agents/README.md](.agents/README.md) for the wiring, and note that Codex only runs
-project-local hooks after you have trusted them with `/hooks`.
+**Before you commit, run `-Scope all`** — or `scripts/preflight.ps1`, which does it for you. That is the only scope
+that reorders members, because ReSharper loads the whole solution either way. Claude Code and Codex run the
+**default scope** on every `.cs` edit through a PostToolUse hook; style and ordering are not run there, because
+they are too slow for a single edit and the build catches them.
## Tests
-xUnit v3 with `[Fact]` / `[Theory]`, assertions via **AwesomeAssertions** (`.Should()`, `Invoking(…)`), fakes via
-**NSubstitute** (plus `NSubstitute.Community.DbConnection` for `DbConnection`/`DbDataReader`), data via
-**AutoFixture**, **Bogus** and **Mapster**, and null-guard coverage via **RentADeveloper.ArgumentNullGuards**.
-
-- Naming: `Method_Scenario_ShouldExpectedOutcome`.
-- Tests derive from `UnitTestsBase` / `IntegrationTestsBase`.
-- One test file per public method, mirroring the `DbConnectionExtensions.*.cs` split.
-
-### The declared public API
+xUnit v3 with `[Fact]` / `[Theory]`, assertions via **AwesomeAssertions**, fakes via **NSubstitute** (plus
+`NSubstitute.Community.DbConnection`), data via **AutoFixture**, **Bogus** and **Mapster**, null-guard coverage via
+**RentADeveloper.ArgumentNullGuards**. Name tests `Method_Scenario_ShouldExpectedOutcome`, derive them from
+`UnitTestsBase` / `IntegrationTestsBase`, one test file per public method, mirroring `DbConnectionExtensions.*.cs`.
-Each of the **six shipping projects** declares its public surface in two files next to its `.csproj`:
+## The declared public API
-| File | Contains |
-|---|---|
-| `PublicAPI.Shipped.txt` | everything that shipped in the last release. Starts with `#nullable enable`, so `string!` and `string?` are different entries. |
-| `PublicAPI.Unshipped.txt` | what is new since then, and `*REMOVED*` lines for what is gone. |
-
-`Microsoft.CodeAnalysis.PublicApiAnalyzers` (wired in `src/Directory.Build.props`) enforces them **at build
-time**: a public member that is not declared is `RS0016`, one that is declared but no longer exists is
-`RS0017`. With `TreatWarningsAsErrors=true` both are build errors, so an accidental change to the public
-surface cannot compile — in an adapter as much as in core.
-
-A deliberate change means recording it:
-
-```bash
-pwsh -File scripts/update-public-api.ps1
-```
-
-That applies the `RS0016` fix, writing the new entries into `PublicAPI.Unshipped.txt` (and creating the two
-files for a project that has none — `dotnet format` will not). **Review the diff**: it is the public-API
-change, and per [CONTRIBUTING.md](CONTRIBUTING.md) it also requires a `CHANGELOG.md` entry, a `README.md`
-update and a SemVer bump in `src/Directory.Build.props`. A `*REMOVED*` line is a break.
-
-At release time, `pwsh -File scripts/update-public-api.ps1 -MarkShipped` folds `Unshipped` into `Shipped`, so
-the next release's `Unshipped.txt` again means "new since the last one".
-
-## Cross-project work: search the whole repo
-
-Changes routinely touch the same symbol across the nine projects in the solution. A search scoped to `src/`
-**will** miss call sites in `tests/` and `benchmarks/` that must be updated in the same change. For scale:
-`EntityHelper.GetEntityTypeMetadata` is used in 18 files spread over eight projects, and the benchmark project
-compiles against the same public surface as the tests do. If a search over a widely used symbol returns a
-handful of hits, suspect the search before believing the count.
-
-Search the whole repository with ripgrep, and let the compiler confirm: `dotnet build DbConnectionPlus.slnx -c
-Release` builds every project, so a call site the search missed is a build error rather than a surprise later.
+Each of the six shipping projects declares its public surface next to its `.csproj`, in `PublicAPI.Shipped.txt`
+(what shipped in the last release) and `PublicAPI.Unshipped.txt` (what is new since, plus `*REMOVED*`
+lines for what is gone). `Microsoft.CodeAnalysis.PublicApiAnalyzers` enforces them **at build time**: `RS0016` for
+an undeclared public member, `RS0017` for a declared one that is gone, both errors, so an accidental change to the
+public surface cannot compile. Record a deliberate one with `pwsh -File scripts/update-public-api.ps1`, then
+**review the diff**: it is the public-API change. Per
+[CONTRIBUTING.md](CONTRIBUTING.md) it also needs a `CHANGELOG.md` entry, a `README.md` update and a SemVer bump in
+`src/Directory.Build.props`. A `*REMOVED*` line is a break. `-MarkShipped` folds `Unshipped` into `Shipped` at
+release time.
## Native AOT support
-The **reflection paths are AOT-safe**: no companion package, no source generator, no consumer opt-in. AOT users
-reference DbConnectionPlus and publish.
-
-- Runtime *code generation* is what Native AOT forbids — plain reflection is fine. So accessors go through
- `System.Reflection.MethodInvoker` and complex-object temporary tables through `EnumerableReader`, both single
- path; the expression-compiled materializer is the JIT fast path only, behind a
- `RuntimeFeature.IsDynamicCodeSupported` branch that the AOT compiler folds away. Do not introduce a
- dependency that emits IL (`Fasterflect`, `FastMember` and friends) — it would be invisible to the analyzers
- and break every AOT consumer.
-- ⚠️ **Correctness constraint:** under trimming, missing `[DynamicallyAccessedMembers]` annotations make
- reflection return *fewer members with no error* — measured: 6 columns in, 0 bound, no exception. Three
- layers defend this: DAM annotations, **treating an `IL2xxx` warning as a defect rather than as noise to
- suppress** (see the code-style section for the two sanctioned exceptions), and a zero-binding guard.
- The defect is invisible on the JIT, so **only `scripts/verify-package-aot.ps1` catches it** — run it after
- any change to a reflection path.
-- The generic query methods carry **neither** `[RequiresUnreferencedCode]` nor `[RequiresDynamicCode]`, so a
- consumer publishing with `PublishAot` or `PublishTrimmed` sees no diagnostic at a call site. The three
- reflection sites the analyzers report inside the library are answered where they occur — the argument is in
- [No consumer-facing diagnostics](DESIGN-DECISIONS.md#4-no-consumer-facing-diagnostics). Adding either
- attribute to a public API is a regression the AOT consumer's zero-diagnostic gate fails on.
-
-A source-generator design was prototyped, benchmarked and **rejected**. Rationale, measurements and the full
-design record: [DESIGN-DECISIONS.md](DESIGN-DECISIONS.md#native-aot-and-trimming). Do not reintroduce it.
-
-Before changing anything that reflects, use the `aot_compat_reviewer` Codex custom agent or walk
-[its checklist](.agents/references/reviews/aot-compat.md).
+The **reflection paths are AOT-safe**: no companion package, no source generator, no consumer opt-in. Rationale and
+measurements: [DESIGN-DECISIONS.md](DESIGN-DECISIONS.md#native-aot-and-trimming). Before changing anything that
+reflects, run the AOT/trim reviewer or walk [its checklist](.agents/references/reviews/aot-compat.md). Three
+constraints must not be broken:
+
+- **Nothing may generate code at run time** — that, not reflection, is what Native AOT forbids. Accessors use
+ `System.Reflection.MethodInvoker`, complex-object temporary tables use `EnumerableReader`, and the
+ expression-compiled materializer sits behind a `RuntimeFeature.IsDynamicCodeSupported` branch the AOT compiler
+ folds away. Add no IL-emitting dependency, and do not reintroduce the prototyped-and-rejected source generator.
+- ⚠️ **The `[DynamicallyAccessedMembers]` chain must stay complete.** Under trimming a missing annotation makes
+ reflection return *fewer members with no error* — measured: 6 columns in, 0 bound, no exception. It is invisible
+ on the JIT, so **only `scripts/verify-package-aot.ps1` catches it**. That is why an `IL2xxx` warning is never
+ suppressed; the code-style rules name the two sanctioned exceptions.
+- **No consumer-facing diagnostics.** The generic query methods carry neither `[RequiresUnreferencedCode]` nor
+ `[RequiresDynamicCode]`; adding either to a public API is a regression the AOT consumer's zero-diagnostic gate
+ fails on. The argument: [DESIGN-DECISIONS.md](DESIGN-DECISIONS.md#4-no-consumer-facing-diagnostics).
## Conventions
-- Branches: `feature/-` or `bugfix/-`, PR'd into `main`.
+- Branches: [Conventional Branch](https://conventionalbranch.org/) — `/issue--` off `main`,
+ types `feature/ bugfix/ hotfix/ release/ chore/`. An agent on its own branch may use `claude/` or `codex/`.
- Commits: Conventional Commits — `feat:`, `fix:`, `BREAKING CHANGE:`. Full checklist:
- [the `commit` skill](.agents/skills/commit/SKILL.md).
-- `CHANGELOG.md` follows [Keep a Changelog](https://keepachangelog.com/); versioning is SemVer.
+ [the `commit` skill](.agents/skills/commit/SKILL.md). `CHANGELOG.md` follows
+ [Keep a Changelog](https://keepachangelog.com/); versioning is SemVer.
+- Pull request process: [CONTRIBUTING.md](CONTRIBUTING.md#pull-request-process).
### Releases
-**Never `dotnet pack` and push by hand.** A release is a pushed tag, and CI does the rest:
-
-1. Bump `` in `src/Directory.Build.props` — the single source of truth for all six packages.
-2. Give the `CHANGELOG.md` section a real date: `## [4.1.0] - 2026-08-17`. Not `TBD`, not empty — CI reads
- this section, uses it as the release notes, and refuses to publish without it.
-3. Merge to `main`, then push the matching tag: `git tag v4.1.0 && git push origin v4.1.0`.
-
-`ci.yml` then verifies the tag against the packed version, publishes all six packages to NuGet.org, and
-creates the GitHub release with the changelog section and the packages attached. Nothing reaches NuGet.org
-that has not passed the lint, test, Native AOT and .NET 8 consumer gates first. A tag whose version does not
-match `src/Directory.Build.props` fails the publish job rather than shipping stale packages.
+**Never `dotnet pack` and push by hand.** A release is a pushed tag and CI does the rest: it checks the tag against
+the packed version, publishes all six packages to NuGet.org, and creates the GitHub release from the `CHANGELOG.md`
+section. The three steps: [CONTRIBUTING.md](CONTRIBUTING.md#releasing).
## Working procedures
-Native Codex skills and custom agents are checked into the repository — see
-[.agents/README.md](.agents/README.md) for the layout and Claude Code compatibility wrappers.
-
-| Task | Codex | Claude Code |
-|---|---|---|
-| Commit changes | `$commit` | `/commit` |
-| Run the integration DBs | `$integration-db` | `/integration-db` |
-| Review AOT/trim safety | `aot_compat_reviewer` custom agent | `aot-compat-reviewer` subagent |
-| Review adapter parity | `adapter_parity_reviewer` custom agent | `adapter-parity-reviewer` subagent |
+Two skills and two review agents are checked in, each under a Codex and a Claude name: `$commit` / `/commit`,
+`$integration-db` / `/integration-db`, `aot_compat_reviewer` / `aot-compat-reviewer`, `adapter_parity_reviewer` /
+`adapter-parity-reviewer`.
-Claude Code (`.claude/settings.json`) and Codex (`.codex/hooks.json`) both fire the same two PostToolUse hooks —
-formatting and the public-API reminder — and both delegate to `scripts/`. Codex needs those hooks trusted once
-per clone (`/hooks`); until then nothing fires and you run `scripts/format-cs.ps1` yourself.
-`scripts/preflight.ps1` before committing is on you under every agent.
+Claude Code (`.claude/settings.json`) and Codex (`.codex/hooks.json`) fire the same two PostToolUse hooks —
+formatting and the public-API reminder — and both delegate to `scripts/`. Codex needs those hooks trusted once per
+clone (`/hooks`); until then nothing fires and you run `scripts/tidy-cs.ps1` yourself.
-Keep reusable skills and review checklists in `.agents/`, Codex metadata and hook wiring in `.codex/`, Claude
-Code metadata and hook wiring in `.claude/`, and executable checks in `scripts/`. Do not fork a procedure or
-check into a tool-specific copy — both integrations must follow the same canonical instructions and scripts.
+Reusable skills and reference material belong in `.agents/`, executable checks in `scripts/`, and only metadata and
+hook wiring in `.codex/` and `.claude/`. Never fork a procedure or a check into a tool-specific copy — both
+integrations follow the same canonical files. Layout: [.agents/README.md](.agents/README.md).
diff --git a/CLAUDE.md b/CLAUDE.md
index dd35cd1..8b6907f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,11 +1,5 @@
# CLAUDE.md
-Read [AGENTS.md](AGENTS.md) first. It is the canonical repository guidance and everything in it applies to
-Claude Code.
+Read [AGENTS.md](AGENTS.md) first — it is the canonical guidance, and all of it applies to Claude Code.
-The files under `.claude/skills/` and `.claude/agents/` contain only Claude Code discovery metadata and point
-to their shared procedures under `.agents/`. Keep reusable instructions in those shared files, not here or in
-the wrappers.
-
-`.claude/settings.json` wires the PostToolUse adapters under `.claude/hooks/` to the canonical implementations
-in `scripts/`. Change hook behavior in `scripts/`; keep `.claude/hooks/` limited to Claude's hook protocol.
+`.claude/` holds only discovery metadata and hook adapters; the procedures live in `.agents/` and `scripts/`.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1e40540..49efa35 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,16 +7,28 @@ Please note we have a code of conduct, please follow it in all your interactions
## Pull Request Process
-1. Branch from `main` as `feature/-` or `bugfix/-`, and use
- [Conventional Commits](https://www.conventionalcommits.org/) for the messages.
+1. Branch from `main` following [Conventional Branch](https://conventionalbranch.org/):
+ `/`, or `/issue--` when there is an issue — for example
+ `feature/issue-42-bulk-insert`. The types are `feature/`, `bugfix/`, `hotfix/`, `release/` and
+ `chore/`; use the long forms, not `feat/` or `fix/`. Descriptions are lowercase letters, digits and
+ hyphens, with no hyphen at the start or end and never two in a row. An AI agent working on its own
+ branch may use `claude/` or `codex/` in place of a type.
+
+ Use [Conventional Commits](https://www.conventionalcommits.org/) for the messages.
2. Make the change, with tests. New behavior and fixed bugs need coverage; a change to one database adapter
almost always has to be mirrored into the other four.
-3. Run the pre-commit gate. It formats, builds and runs the unit suite:
+3. Run the pre-commit gate. It applies style, formatting and member ordering, then builds and runs the
+ unit suite:
```shell
pwsh -File scripts/preflight.ps1
```
- `TreatWarningsAsErrors` is on for the six shipping projects, so the build is also the style, trim-analyzer
- and public-API gate. **Never suppress an `IL2xxx` warning to get a green build** — it is the only
+ It rewrites files — review what it changed and include it in your commit. To run just the tidying:
+ `pwsh -File scripts/tidy-cs.ps1 -Scope all`.
+
+ `TreatWarningsAsErrors` is on for **every** project, so the build is also the style, member-ordering,
+ trim-analyzer and public-API gate, and `CSharpier.MsBuild` makes an unformatted file a build error too.
+ The build never rewrites your files — it fails and names them; `scripts/tidy-cs.ps1` is what fixes them.
+ **Never suppress an `IL2xxx` warning to get a green build** — it is the only
build-time evidence that the trimming annotations are complete.
4. If you touched a reflection path, also run the Native AOT gate. Nothing else in the repository can see
silent trimming damage:
@@ -36,6 +48,24 @@ Please note we have a code of conduct, please follow it in all your interactions
The full working guide — layout, the adapter seam, code style, the declared public API and Native AOT — is
[AGENTS.md](AGENTS.md). It is written for AI coding agents, but everything in it applies to people too.
+## Setting up a clone
+
+Two things to do once, after cloning:
+
+```shell
+dotnet tool restore
+git config blame.ignoreRevsFile .git-blame-ignore-revs
+```
+
+The first installs CSharpier, the ReSharper command line tools and docfx, which `scripts/tidy-cs.ps1` and the
+documentation build need. The second makes `git blame` skip the commits listed in `.git-blame-ignore-revs`,
+which reformatted and reordered the whole repository, so blame points at whoever wrote the logic rather than
+at the tool that moved it. GitHub already does this on its own.
+
+If you use Rider, two settings make the tooling invisible: install the **CSharpier** plugin and switch on
+Settings | Tools | CSharpier | Run on Save, and use the shared **ReorderMembers** cleanup profile (from
+`DbConnectionPlus.slnx.DotSettings`) when you want members put back in order.
+
## Releasing
Releases are cut by CI from a pushed tag; nothing is packed or pushed by hand. The versioning scheme is
diff --git a/DbConnectionPlus.sln.DotSettings b/DbConnectionPlus.sln.DotSettings
deleted file mode 100644
index 763f22a..0000000
--- a/DbConnectionPlus.sln.DotSettings
+++ /dev/null
@@ -1,27 +0,0 @@
-
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
\ No newline at end of file
diff --git a/DbConnectionPlus.slnx.DotSettings b/DbConnectionPlus.slnx.DotSettings
new file mode 100644
index 0000000..54a5ad3
--- /dev/null
+++ b/DbConnectionPlus.slnx.DotSettings
@@ -0,0 +1,255 @@
+
+ <?xml version="1.0" encoding="utf-16"?>
+<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
+ <TypePattern DisplayName="Types marked [NoReorder]" Priority="100">
+ <TypePattern.Match>
+ <HasAttribute Name="JetBrains.Annotations.NoReorderAttribute" />
+ </TypePattern.Match>
+ </TypePattern>
+ <TypePattern DisplayName="StyleCop order" RemoveRegions="None">
+ <Entry DisplayName="Constants">
+ <Entry.Match>
+ <Kind Is="Constant" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Fields">
+ <Entry.Match>
+ <Kind Is="Field" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Constructors">
+ <Entry.Match>
+ <Kind Is="Constructor" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Static />
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Finalizers">
+ <Entry.Match>
+ <Kind Is="Destructor" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Delegates">
+ <Entry.Match>
+ <Kind Is="Delegate" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Events">
+ <Entry.Match>
+ <Kind Is="Event" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Enums">
+ <Entry.Match>
+ <Kind Is="Enum" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Interfaces">
+ <Entry.Match>
+ <Kind Is="Interface" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Explicit interface properties">
+ <Entry.Match>
+ <And>
+ <Kind Is="Property" />
+ <ImplementsInterface />
+ <Access Is="Private" />
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Properties">
+ <Entry.Match>
+ <Kind Is="Property" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Explicit interface indexers">
+ <Entry.Match>
+ <And>
+ <Kind Is="Indexer" />
+ <ImplementsInterface />
+ <Access Is="Private" />
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Indexers">
+ <Entry.Match>
+ <Kind Is="Indexer" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Conversion operators">
+ <Entry.Match>
+ <And>
+ <Kind Is="Operator" />
+ <Or>
+ <Name Is="op_Implicit" />
+ <Name Is="op_Explicit" />
+ </Or>
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Operators">
+ <Entry.Match>
+ <Kind Is="Operator" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Explicit interface methods">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <ImplementsInterface />
+ <Access Is="Private" />
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Methods">
+ <Entry.Match>
+ <Kind Is="Method" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Structs">
+ <Entry.Match>
+ <Kind Is="Struct" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Classes">
+ <Entry.Match>
+ <Kind Is="Class" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Access Order="Public Internal ProtectedInternal Protected PrivateProtected Private" />
+ <Static />
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ </TypePattern>
+</Patterns>
+ <?xml version="1.0" encoding="utf-16"?><Profile name="ReorderMembers"><CSReorderTypeMembers>True</CSReorderTypeMembers></Profile>
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
diff --git a/Directory.Build.props b/Directory.Build.props
index 5260d22..cfc2df9 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -23,12 +23,54 @@
true
+
+ True
+ true
+
+
+ true
+
+
+
+
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
@@ -37,10 +79,29 @@
allruntime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/AotJobFilter.cs b/benchmarks/DbConnectionPlus.Benchmarks/AotJobFilter.cs
index 68c0b82..cf9747f 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/AotJobFilter.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/AotJobFilter.cs
@@ -13,16 +13,8 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
// file for the measurements behind that.
public class AotJobFilter : IFilter
{
- public Boolean Predicate(BenchmarkCase benchmarkCase)
- {
- var isAotJob = benchmarkCase.Job.Id.Contains(BenchmarksConfig.AotJobId, StringComparison.Ordinal);
- var benchmarkName = benchmarkCase.Descriptor.WorkloadMethod.Name;
- var isAotOnlyBenchmark = benchmarkName.EndsWith(AotOnlyBenchmarkSuffix, StringComparison.Ordinal);
-
- return isAotJob
- ? AotJobBenchmarks.Contains(benchmarkName)
- : !isAotOnlyBenchmark;
- }
+ // Marks a benchmark as Native AOT only, so that it is kept out of the JIT job.
+ private const string AotOnlyBenchmarkSuffix = "_Aot";
// TemporaryTable_ComplexObjects is here because it ends in a Query over the temporary
// table: its write path is single path, its read path is not.
@@ -31,7 +23,7 @@ public Boolean Predicate(BenchmarkCase benchmarkCase)
// materializers with Reflection.Emit - so the only way to have it here is the Dapper.AOT build-time
// generator, and that generator handles neither value tuples nor Dapper.Contrib, which is what the other
// two categories compare against.
- private static readonly HashSet AotJobBenchmarks =
+ private static readonly HashSet AotJobBenchmarks =
[
nameof(Benchmarks.Query_Entities_Command),
nameof(Benchmarks.Query_Entities_Dapper_Aot),
@@ -39,9 +31,15 @@ public Boolean Predicate(BenchmarkCase benchmarkCase)
nameof(Benchmarks.Query_ValueTuples_Command),
nameof(Benchmarks.Query_ValueTuples_DbConnectionPlus),
nameof(Benchmarks.TemporaryTable_ComplexObjects_Command),
- nameof(Benchmarks.TemporaryTable_ComplexObjects_DbConnectionPlus)
+ nameof(Benchmarks.TemporaryTable_ComplexObjects_DbConnectionPlus),
];
- // Marks a benchmark as Native AOT only, so that it is kept out of the JIT job.
- private const String AotOnlyBenchmarkSuffix = "_Aot";
+ public bool Predicate(BenchmarkCase benchmarkCase)
+ {
+ var isAotJob = benchmarkCase.Job.Id.Contains(BenchmarksConfig.AotJobId, StringComparison.Ordinal);
+ var benchmarkName = benchmarkCase.Descriptor.WorkloadMethod.Name;
+ var isAotOnlyBenchmark = benchmarkName.EndsWith(AotOnlyBenchmarkSuffix, StringComparison.Ordinal);
+
+ return isAotJob ? AotJobBenchmarks.Contains(benchmarkName) : !isAotOnlyBenchmark;
+ }
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs
index 2a83f7a..8542d41 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntities.cs
@@ -7,33 +7,19 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(DeleteEntities_Command),
- nameof(DeleteEntities_Dapper),
- nameof(DeleteEntities_DbConnectionPlus)
- ]
- )]
- public void DeleteEntities__Cleanup() =>
- this.connection.Dispose();
+ private const string DeleteEntities_Category = "DeleteEntities";
+ private const int DeleteEntities_EntitiesPerOperation = 250;
- [GlobalSetup(
- Targets =
- [
- nameof(DeleteEntities_Command),
- nameof(DeleteEntities_Dapper),
- nameof(DeleteEntities_DbConnectionPlus)
- ]
- )]
- public void DeleteEntities__Setup()
- {
- this.SetupDatabase(DeleteEntities_EntitiesPerOperation * DeleteEntities_OperationsPerInvoke);
+ // Batches per invocation: one reported operation is one delete call over
+ // DeleteEntities_EntitiesPerOperation entities.
+ //
+ // The transaction is rolled back rather than committed, so every invocation puts the rows back. See
+ // DeleteEntity_OperationsPerInvoke for why that matters and for the measurement showing a rollback costs
+ // what a commit costs. Twenty batches is 5 000 seeded rows, down from 75 000, and it amortizes the
+ // transaction far past the point where it could affect the ratios.
+ private const int DeleteEntities_OperationsPerInvoke = 20;
- // The batches are built once here so that the benchmarks do not slice the entity list inside the measured
- // region. The slicing was identical for all three implementations and therefore only compressed the ratios.
- this.deleteEntities_batches = [.. this.entitiesInDb.Chunk(DeleteEntities_EntitiesPerOperation)];
- }
+ private List deleteEntities_batches = null!;
[Benchmark(Baseline = true, OperationsPerInvoke = DeleteEntities_OperationsPerInvoke)]
[BenchmarkCategory(DeleteEntities_Category)]
@@ -93,17 +79,28 @@ public void DeleteEntities_DbConnectionPlus()
transaction.Rollback();
}
- private List deleteEntities_batches = null!;
+ [GlobalCleanup(
+ Targets = [
+ nameof(DeleteEntities_Command),
+ nameof(DeleteEntities_Dapper),
+ nameof(DeleteEntities_DbConnectionPlus),
+ ]
+ )]
+ public void DeleteEntities__Cleanup() => this.connection.Dispose();
- private const String DeleteEntities_Category = "DeleteEntities";
- private const Int32 DeleteEntities_EntitiesPerOperation = 250;
+ [GlobalSetup(
+ Targets = [
+ nameof(DeleteEntities_Command),
+ nameof(DeleteEntities_Dapper),
+ nameof(DeleteEntities_DbConnectionPlus),
+ ]
+ )]
+ public void DeleteEntities__Setup()
+ {
+ this.SetupDatabase(DeleteEntities_EntitiesPerOperation * DeleteEntities_OperationsPerInvoke);
- // Batches per invocation: one reported operation is one delete call over
- // DeleteEntities_EntitiesPerOperation entities.
- //
- // The transaction is rolled back rather than committed, so every invocation puts the rows back. See
- // DeleteEntity_OperationsPerInvoke for why that matters and for the measurement showing a rollback costs
- // what a commit costs. Twenty batches is 5 000 seeded rows, down from 75 000, and it amortizes the
- // transaction far past the point where it could affect the ratios.
- private const Int32 DeleteEntities_OperationsPerInvoke = 20;
+ // The batches are built once here so that the benchmarks do not slice the entity list inside the measured
+ // region. The slicing was identical for all three implementations and therefore only compressed the ratios.
+ this.deleteEntities_batches = [.. this.entitiesInDb.Chunk(DeleteEntities_EntitiesPerOperation)];
+ }
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntity.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntity.cs
index f3d6058..1b6751c 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntity.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.DeleteEntity.cs
@@ -7,27 +7,25 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(DeleteEntity_Command),
- nameof(DeleteEntity_Dapper),
- nameof(DeleteEntity_DbConnectionPlus)
- ]
- )]
- public void DeleteEntity__Cleanup() =>
- this.connection.Dispose();
+ private const string DeleteEntity_Category = "DeleteEntity";
- [GlobalSetup(
- Targets =
- [
- nameof(DeleteEntity_Command),
- nameof(DeleteEntity_Dapper),
- nameof(DeleteEntity_DbConnectionPlus)
- ]
- )]
- public void DeleteEntity__Setup() =>
- this.SetupDatabase(DeleteEntity_OperationsPerInvoke);
+ // Deletes per invocation, and also the number of rows seeded into the table.
+ //
+ // The transaction is rolled back rather than committed, so every invocation puts the rows back and the next
+ // one deletes them again. That is what lets this category drop [IterationSetup], and dropping it is the
+ // point: an iteration setup pins InvocationCount to 1, which makes the iteration time a function of this
+ // constant and of how fast the machine is - and BenchmarkDotNet then warns that the iteration is too short
+ // to measure reliably. Without it the pilot stage tunes the invocation count by itself, on any machine.
+ //
+ // Rolling back costs the same as committing: measured on this schema in an in-memory SQLite database at 1,
+ // 100, 1 000, 10 000 and 40 000 rows, the rollback/commit ratio stayed between 0.98x and 1.03x with no
+ // trend - and it is identical for all three implementations either way.
+ //
+ // It is 1 000 rather than 1 because BeginTransaction plus Rollback costs roughly 3 us against roughly
+ // 0.5 us for the marginal delete. At one delete per invocation that fixed cost would be about two thirds
+ // of the measurement - not a bias, since all three implementations pay it, but it would compress the
+ // ratios this benchmark exists to show. Amortized over 1 000 deletes it is well under 1 %.
+ private const int DeleteEntity_OperationsPerInvoke = 1000;
[Benchmark(Baseline = true, OperationsPerInvoke = DeleteEntity_OperationsPerInvoke)]
[BenchmarkCategory(DeleteEntity_Category)]
@@ -83,23 +81,13 @@ public void DeleteEntity_DbConnectionPlus()
transaction.Rollback();
}
- private const String DeleteEntity_Category = "DeleteEntity";
+ [GlobalCleanup(
+ Targets = [nameof(DeleteEntity_Command), nameof(DeleteEntity_Dapper), nameof(DeleteEntity_DbConnectionPlus)]
+ )]
+ public void DeleteEntity__Cleanup() => this.connection.Dispose();
- // Deletes per invocation, and also the number of rows seeded into the table.
- //
- // The transaction is rolled back rather than committed, so every invocation puts the rows back and the next
- // one deletes them again. That is what lets this category drop [IterationSetup], and dropping it is the
- // point: an iteration setup pins InvocationCount to 1, which makes the iteration time a function of this
- // constant and of how fast the machine is - and BenchmarkDotNet then warns that the iteration is too short
- // to measure reliably. Without it the pilot stage tunes the invocation count by itself, on any machine.
- //
- // Rolling back costs the same as committing: measured on this schema in an in-memory SQLite database at 1,
- // 100, 1 000, 10 000 and 40 000 rows, the rollback/commit ratio stayed between 0.98x and 1.03x with no
- // trend - and it is identical for all three implementations either way.
- //
- // It is 1 000 rather than 1 because BeginTransaction plus Rollback costs roughly 3 us against roughly
- // 0.5 us for the marginal delete. At one delete per invocation that fixed cost would be about two thirds
- // of the measurement - not a bias, since all three implementations pay it, but it would compress the
- // ratios this benchmark exists to show. Amortized over 1 000 deletes it is well under 1 %.
- private const Int32 DeleteEntity_OperationsPerInvoke = 1000;
+ [GlobalSetup(
+ Targets = [nameof(DeleteEntity_Command), nameof(DeleteEntity_Dapper), nameof(DeleteEntity_DbConnectionPlus)]
+ )]
+ public void DeleteEntity__Setup() => this.SetupDatabase(DeleteEntity_OperationsPerInvoke);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs
index 9dfba35..6306468 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteNonQuery.cs
@@ -7,27 +7,7 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(ExecuteNonQuery_Command),
- nameof(ExecuteNonQuery_Dapper),
- nameof(ExecuteNonQuery_DbConnectionPlus)
- ]
- )]
- public void ExecuteNonQuery__Cleanup() =>
- this.connection.Dispose();
-
- [GlobalSetup(
- Targets =
- [
- nameof(ExecuteNonQuery_Command),
- nameof(ExecuteNonQuery_Dapper),
- nameof(ExecuteNonQuery_DbConnectionPlus)
- ]
- )]
- public void ExecuteNonQuery__Setup() =>
- this.SetupDatabase(0);
+ private const string ExecuteNonQuery_Category = "ExecuteNonQuery";
[Benchmark(Baseline = true)]
[BenchmarkCategory(ExecuteNonQuery_Category)]
@@ -57,5 +37,21 @@ public void ExecuteNonQuery_Dapper() =>
public void ExecuteNonQuery_DbConnectionPlus() =>
this.connection.ExecuteNonQuery($"DELETE FROM Entity WHERE Id = {Parameter(-1)}");
- private const String ExecuteNonQuery_Category = "ExecuteNonQuery";
+ [GlobalCleanup(
+ Targets = [
+ nameof(ExecuteNonQuery_Command),
+ nameof(ExecuteNonQuery_Dapper),
+ nameof(ExecuteNonQuery_DbConnectionPlus),
+ ]
+ )]
+ public void ExecuteNonQuery__Cleanup() => this.connection.Dispose();
+
+ [GlobalSetup(
+ Targets = [
+ nameof(ExecuteNonQuery_Command),
+ nameof(ExecuteNonQuery_Dapper),
+ nameof(ExecuteNonQuery_DbConnectionPlus),
+ ]
+ )]
+ public void ExecuteNonQuery__Setup() => this.SetupDatabase(0);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteReader.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteReader.cs
index 2d1db3c..e4b3c44 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteReader.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteReader.cs
@@ -7,27 +7,7 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(ExecuteReader_Command),
- nameof(ExecuteReader_Dapper),
- nameof(ExecuteReader_DbConnectionPlus)
- ]
- )]
- public void ExecuteReader__Cleanup() =>
- this.connection.Dispose();
-
- [GlobalSetup(
- Targets =
- [
- nameof(ExecuteReader_Command),
- nameof(ExecuteReader_Dapper),
- nameof(ExecuteReader_DbConnectionPlus)
- ]
- )]
- public void ExecuteReader__Setup() =>
- this.SetupDatabase(100);
+ private const string ExecuteReader_Category = "ExecuteReader";
[Benchmark(Baseline = true)]
[BenchmarkCategory(ExecuteReader_Category)]
@@ -81,5 +61,13 @@ public List ExecuteReader_DbConnectionPlus()
return result;
}
- private const String ExecuteReader_Category = "ExecuteReader";
+ [GlobalCleanup(
+ Targets = [nameof(ExecuteReader_Command), nameof(ExecuteReader_Dapper), nameof(ExecuteReader_DbConnectionPlus)]
+ )]
+ public void ExecuteReader__Cleanup() => this.connection.Dispose();
+
+ [GlobalSetup(
+ Targets = [nameof(ExecuteReader_Command), nameof(ExecuteReader_Dapper), nameof(ExecuteReader_DbConnectionPlus)]
+ )]
+ public void ExecuteReader__Setup() => this.SetupDatabase(100);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs
index b56cc05..84e9261 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.ExecuteScalar.cs
@@ -7,31 +7,11 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(ExecuteScalar_Command),
- nameof(ExecuteScalar_Dapper),
- nameof(ExecuteScalar_DbConnectionPlus)
- ]
- )]
- public void ExecuteScalar__Cleanup() =>
- this.connection.Dispose();
-
- [GlobalSetup(
- Targets =
- [
- nameof(ExecuteScalar_Command),
- nameof(ExecuteScalar_Dapper),
- nameof(ExecuteScalar_DbConnectionPlus)
- ]
- )]
- public void ExecuteScalar__Setup() =>
- this.SetupDatabase(1);
+ private const string ExecuteScalar_Category = "ExecuteScalar";
[Benchmark(Baseline = true)]
[BenchmarkCategory(ExecuteScalar_Category)]
- public String ExecuteScalar_Command()
+ public string ExecuteScalar_Command()
{
var entity = this.entitiesInDb[0];
@@ -45,16 +25,16 @@ public String ExecuteScalar_Command()
command.Parameters.Add(idParameter);
- return (String)command.ExecuteScalar()!;
+ return (string)command.ExecuteScalar()!;
}
[Benchmark(Baseline = false)]
[BenchmarkCategory(ExecuteScalar_Category)]
- public String ExecuteScalar_Dapper()
+ public string ExecuteScalar_Dapper()
{
var entity = this.entitiesInDb[0];
- return SqlMapper.ExecuteScalar(
+ return SqlMapper.ExecuteScalar(
this.connection,
"SELECT StringValue FROM Entity WHERE Id = @Id",
new { entity.Id }
@@ -63,14 +43,22 @@ public String ExecuteScalar_Dapper()
[Benchmark(Baseline = false)]
[BenchmarkCategory(ExecuteScalar_Category)]
- public String ExecuteScalar_DbConnectionPlus()
+ public string ExecuteScalar_DbConnectionPlus()
{
var entity = this.entitiesInDb[0];
- return this.connection.ExecuteScalar(
+ return this.connection.ExecuteScalar(
$"SELECT StringValue FROM Entity WHERE Id = {Parameter(entity.Id)}"
);
}
- private const String ExecuteScalar_Category = "ExecuteScalar";
+ [GlobalCleanup(
+ Targets = [nameof(ExecuteScalar_Command), nameof(ExecuteScalar_Dapper), nameof(ExecuteScalar_DbConnectionPlus)]
+ )]
+ public void ExecuteScalar__Cleanup() => this.connection.Dispose();
+
+ [GlobalSetup(
+ Targets = [nameof(ExecuteScalar_Command), nameof(ExecuteScalar_Dapper), nameof(ExecuteScalar_DbConnectionPlus)]
+ )]
+ public void ExecuteScalar__Setup() => this.SetupDatabase(1);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs
index 65e6233..064d0fe 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Exists.cs
@@ -7,31 +7,11 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(Exists_Command),
- nameof(Exists_Dapper),
- nameof(Exists_DbConnectionPlus)
- ]
- )]
- public void Exists__Cleanup() =>
- this.connection.Dispose();
-
- [GlobalSetup(
- Targets =
- [
- nameof(Exists_Command),
- nameof(Exists_Dapper),
- nameof(Exists_DbConnectionPlus)
- ]
- )]
- public void Exists__Setup() =>
- this.SetupDatabase(1);
+ private const string Exists_Category = "Exists";
[Benchmark(Baseline = true)]
[BenchmarkCategory(Exists_Category)]
- public Boolean Exists_Command()
+ public bool Exists_Command()
{
var entityId = this.entitiesInDb[0].Id;
@@ -51,7 +31,7 @@ public Boolean Exists_Command()
[Benchmark(Baseline = false)]
[BenchmarkCategory(Exists_Category)]
- public Boolean Exists_Dapper()
+ public bool Exists_Dapper()
{
var entityId = this.entitiesInDb[0].Id;
@@ -66,12 +46,16 @@ public Boolean Exists_Dapper()
[Benchmark(Baseline = false)]
[BenchmarkCategory(Exists_Category)]
- public Boolean Exists_DbConnectionPlus()
+ public bool Exists_DbConnectionPlus()
{
var entityId = this.entitiesInDb[0].Id;
return this.connection.Exists($"SELECT 1 FROM Entity WHERE Id = {Parameter(entityId)}");
}
- private const String Exists_Category = "Exists";
+ [GlobalCleanup(Targets = [nameof(Exists_Command), nameof(Exists_Dapper), nameof(Exists_DbConnectionPlus)])]
+ public void Exists__Cleanup() => this.connection.Dispose();
+
+ [GlobalSetup(Targets = [nameof(Exists_Command), nameof(Exists_Dapper), nameof(Exists_DbConnectionPlus)])]
+ public void Exists__Setup() => this.SetupDatabase(1);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntities.cs
index 839da7a..2c4d7f3 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntities.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntities.cs
@@ -7,27 +7,51 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(InsertEntities_Command),
- nameof(InsertEntities_Dapper),
- nameof(InsertEntities_DbConnectionPlus)
- ]
- )]
- public void InsertEntities__Cleanup() =>
- this.connection.Dispose();
-
- [GlobalSetup(
- Targets =
- [
- nameof(InsertEntities_Command),
- nameof(InsertEntities_Dapper),
- nameof(InsertEntities_DbConnectionPlus)
- ]
- )]
- public void InsertEntities__Setup() =>
- this.SetupDatabase(0);
+ private const string InsertEntities_Category = "InsertEntities";
+ private const int InsertEntities_EntitiesPerOperation = 200;
+
+ private const string InsertEntitySql = """
+ INSERT INTO Entity
+ (
+ Id,
+ BooleanValue,
+ BytesValue,
+ ByteValue,
+ CharValue,
+ DateTimeValue,
+ DecimalValue,
+ DoubleValue,
+ EnumValue,
+ Int16Value,
+ Int32Value,
+ Int64Value,
+ SingleValue,
+ StringValue
+ )
+ VALUES
+ (
+ @Id,
+ @BooleanValue,
+ @BytesValue,
+ @ByteValue,
+ @CharValue,
+ @DateTimeValue,
+ @DecimalValue,
+ @DoubleValue,
+ @EnumValue,
+ @Int16Value,
+ @Int32Value,
+ @Int64Value,
+ @SingleValue,
+ @StringValue
+ )
+ """;
+
+ private readonly List insertEntities_entitiesToInsert = Generate.Multiple(
+ InsertEntities_EntitiesPerOperation
+ );
+
+ private long insertEntities_nextId;
[Benchmark(Baseline = true)]
[BenchmarkCategory(InsertEntities_Category)]
@@ -39,7 +63,7 @@ public void InsertEntities_Command()
command.CommandText = InsertEntitySql;
- var parameters = new Dictionary
+ var parameters = new Dictionary
{
{ "Id", new("Id", null) },
{ "BooleanValue", new("BooleanValue", null) },
@@ -54,7 +78,7 @@ public void InsertEntities_Command()
{ "Int32Value", new("Int32Value", null) },
{ "Int64Value", new("Int64Value", null) },
{ "SingleValue", new("SingleValue", null) },
- { "StringValue", new("StringValue", null) }
+ { "StringValue", new("StringValue", null) },
};
command.Parameters.AddRange(parameters.Values);
@@ -85,6 +109,24 @@ public void InsertEntities_DbConnectionPlus()
this.connection.InsertEntities(this.insertEntities_entitiesToInsert);
}
+ [GlobalCleanup(
+ Targets = [
+ nameof(InsertEntities_Command),
+ nameof(InsertEntities_Dapper),
+ nameof(InsertEntities_DbConnectionPlus),
+ ]
+ )]
+ public void InsertEntities__Cleanup() => this.connection.Dispose();
+
+ [GlobalSetup(
+ Targets = [
+ nameof(InsertEntities_Command),
+ nameof(InsertEntities_Dapper),
+ nameof(InsertEntities_DbConnectionPlus),
+ ]
+ )]
+ public void InsertEntities__Setup() => this.SetupDatabase(0);
+
// A fresh key per entity, because Id is the primary key and the benchmarks insert the same set of entities
// over and over into a table that starts out empty. This runs inside the measured region, but it is a few
// hundred nanoseconds of field writes against an operation of several milliseconds, and all three
@@ -96,49 +138,4 @@ private void AssignNextInsertEntitiesIds()
entity.Id = ++this.insertEntities_nextId;
}
}
-
- private readonly List insertEntities_entitiesToInsert =
- Generate.Multiple(InsertEntities_EntitiesPerOperation);
-
- private Int64 insertEntities_nextId;
-
- private const String InsertEntities_Category = "InsertEntities";
- private const Int32 InsertEntities_EntitiesPerOperation = 200;
-
- private const String InsertEntitySql = """
- INSERT INTO Entity
- (
- Id,
- BooleanValue,
- BytesValue,
- ByteValue,
- CharValue,
- DateTimeValue,
- DecimalValue,
- DoubleValue,
- EnumValue,
- Int16Value,
- Int32Value,
- Int64Value,
- SingleValue,
- StringValue
- )
- VALUES
- (
- @Id,
- @BooleanValue,
- @BytesValue,
- @ByteValue,
- @CharValue,
- @DateTimeValue,
- @DecimalValue,
- @DoubleValue,
- @EnumValue,
- @Int16Value,
- @Int32Value,
- @Int64Value,
- @SingleValue,
- @StringValue
- )
- """;
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntity.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntity.cs
index bc229e2..857169a 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntity.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.InsertEntity.cs
@@ -7,27 +7,13 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(InsertEntity_Command),
- nameof(InsertEntity_Dapper),
- nameof(InsertEntity_DbConnectionPlus)
- ]
- )]
- public void InsertEntity__Cleanup() =>
- this.connection.Dispose();
+ private const string InsertEntity_Category = "InsertEntity";
- [GlobalSetup(
- Targets =
- [
- nameof(InsertEntity_Command),
- nameof(InsertEntity_Dapper),
- nameof(InsertEntity_DbConnectionPlus)
- ]
- )]
- public void InsertEntity__Setup() =>
- this.SetupDatabase(0);
+ private readonly BenchmarkEntity insertEntity_entityToInsert = Generate.Single();
+
+ // A fresh key per invocation, because Id is the primary key and the benchmarks insert the same entity over
+ // and over into a table that starts out empty.
+ private long insertEntity_nextId;
[Benchmark(Baseline = true)]
[BenchmarkCategory(InsertEntity_Category)]
@@ -39,7 +25,7 @@ public void InsertEntity_Command()
command.CommandText = InsertEntitySql;
- var parameters = new Dictionary
+ var parameters = new Dictionary
{
{ "Id", new("Id", null) },
{ "BooleanValue", new("BooleanValue", null) },
@@ -54,7 +40,7 @@ public void InsertEntity_Command()
{ "Int32Value", new("Int32Value", null) },
{ "Int64Value", new("Int64Value", null) },
{ "SingleValue", new("SingleValue", null) },
- { "StringValue", new("StringValue", null) }
+ { "StringValue", new("StringValue", null) },
};
command.Parameters.AddRange(parameters.Values);
@@ -82,14 +68,15 @@ public void InsertEntity_DbConnectionPlus()
this.connection.InsertEntity(this.insertEntity_entityToInsert);
}
- private void AssignNextInsertEntityId() =>
- this.insertEntity_entityToInsert.Id = ++this.insertEntity_nextId;
-
- private readonly BenchmarkEntity insertEntity_entityToInsert = Generate.Single();
+ [GlobalCleanup(
+ Targets = [nameof(InsertEntity_Command), nameof(InsertEntity_Dapper), nameof(InsertEntity_DbConnectionPlus)]
+ )]
+ public void InsertEntity__Cleanup() => this.connection.Dispose();
- // A fresh key per invocation, because Id is the primary key and the benchmarks insert the same entity over
- // and over into a table that starts out empty.
- private Int64 insertEntity_nextId;
+ [GlobalSetup(
+ Targets = [nameof(InsertEntity_Command), nameof(InsertEntity_Dapper), nameof(InsertEntity_DbConnectionPlus)]
+ )]
+ public void InsertEntity__Setup() => this.SetupDatabase(0);
- private const String InsertEntity_Category = "InsertEntity";
+ private void AssignNextInsertEntityId() => this.insertEntity_entityToInsert.Id = ++this.insertEntity_nextId;
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs
index 8daba2e..6d23495 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Parameter.cs
@@ -7,31 +7,11 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(Parameter_Command),
- nameof(Parameter_Dapper),
- nameof(Parameter_DbConnectionPlus)
- ]
- )]
- public void Parameter__Cleanup() =>
- this.connection.Dispose();
-
- [GlobalSetup(
- Targets =
- [
- nameof(Parameter_Command),
- nameof(Parameter_Dapper),
- nameof(Parameter_DbConnectionPlus)
- ]
- )]
- public void Parameter__Setup() =>
- this.SetupDatabase(0);
+ private const string Parameter_Category = "Parameter";
[Benchmark(Baseline = true)]
[BenchmarkCategory(Parameter_Category)]
- public Int64 Parameter_Command()
+ public long Parameter_Command()
{
using var command = this.connection.CreateCommand();
@@ -48,27 +28,43 @@ public Int64 Parameter_Command()
command.Parameters.Add(new("@P9", 9));
command.Parameters.Add(new("@P10", 10));
- return (Int64)command.ExecuteScalar()!;
+ return (long)command.ExecuteScalar()!;
}
[Benchmark(Baseline = false)]
[BenchmarkCategory(Parameter_Category)]
- public Int64 Parameter_Dapper() =>
- SqlMapper.ExecuteScalar(
+ public long Parameter_Dapper() =>
+ SqlMapper.ExecuteScalar(
this.connection,
"SELECT @P1 + @P2 + @P3 + @P4 + @P5 + @P6 + @P7 + @P8 + @P9 + @P10",
- new { P1 = 1, P2 = 2, P3 = 3, P4 = 4, P5 = 5, P6 = 6, P7 = 7, P8 = 8, P9 = 9, P10 = 10 }
+ new
+ {
+ P1 = 1,
+ P2 = 2,
+ P3 = 3,
+ P4 = 4,
+ P5 = 5,
+ P6 = 6,
+ P7 = 7,
+ P8 = 8,
+ P9 = 9,
+ P10 = 10,
+ }
);
[Benchmark(Baseline = false)]
[BenchmarkCategory(Parameter_Category)]
- public Int64 Parameter_DbConnectionPlus() =>
- this.connection.ExecuteScalar(
+ public long Parameter_DbConnectionPlus() =>
+ this.connection.ExecuteScalar(
$"""
- SELECT {Parameter(1)} + {Parameter(2)} + {Parameter(3)} + {Parameter(4)} + {Parameter(5)} +
- {Parameter(6)} + {Parameter(7)} + {Parameter(8)} + {Parameter(9)} + {Parameter(10)}
- """
+ SELECT {Parameter(1)} + {Parameter(2)} + {Parameter(3)} + {Parameter(4)} + {Parameter(5)} +
+ {Parameter(6)} + {Parameter(7)} + {Parameter(8)} + {Parameter(9)} + {Parameter(10)}
+ """
);
- private const String Parameter_Category = "Parameter";
+ [GlobalCleanup(Targets = [nameof(Parameter_Command), nameof(Parameter_Dapper), nameof(Parameter_DbConnectionPlus)])]
+ public void Parameter__Cleanup() => this.connection.Dispose();
+
+ [GlobalSetup(Targets = [nameof(Parameter_Command), nameof(Parameter_Dapper), nameof(Parameter_DbConnectionPlus)])]
+ public void Parameter__Setup() => this.SetupDatabase(0);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs
index c3daac9..249e947 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Dynamic.cs
@@ -9,27 +9,8 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(Query_Dynamic_Command),
- nameof(Query_Dynamic_Dapper),
- nameof(Query_Dynamic_DbConnectionPlus)
- ]
- )]
- public void Query_Dynamic__Cleanup() =>
- this.connection.Dispose();
-
- [GlobalSetup(
- Targets =
- [
- nameof(Query_Dynamic_Command),
- nameof(Query_Dynamic_Dapper),
- nameof(Query_Dynamic_DbConnectionPlus)
- ]
- )]
- public void Query_Dynamic__Setup() =>
- this.SetupDatabase(Query_Dynamic_EntitiesPerOperation);
+ private const string Query_Dynamic_Category = "Query_Dynamic";
+ private const int Query_Dynamic_EntitiesPerOperation = 100;
[Benchmark(Baseline = true)]
[BenchmarkCategory(Query_Dynamic_Category)]
@@ -41,28 +22,29 @@ public List Query_Dynamic_Command()
while (dataReader.Read())
{
- var charBuffer = new Char[1];
+ var charBuffer = new char[1];
var ordinal = 0;
- var dictionary = new Dictionary
+ var dictionary = new Dictionary
{
["Id"] = dataReader.GetInt64(ordinal++),
["BooleanValue"] = dataReader.GetInt64(ordinal++) == 1,
- ["BytesValue"] = (Byte[])dataReader.GetValue(ordinal++),
+ ["BytesValue"] = (byte[])dataReader.GetValue(ordinal++),
["ByteValue"] = dataReader.GetByte(ordinal++),
- ["CharValue"] = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1
- ? charBuffer[0]
- : throw new InvalidOperationException(),
+ ["CharValue"] =
+ dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1
+ ? charBuffer[0]
+ : throw new InvalidOperationException(),
["DateTimeValue"] = DateTime.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture),
- ["DecimalValue"] = Decimal.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture),
+ ["DecimalValue"] = decimal.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture),
["DoubleValue"] = dataReader.GetDouble(ordinal++),
["EnumValue"] = Enum.Parse(dataReader.GetString(ordinal++)),
- ["Int16Value"] = (Int16)dataReader.GetInt64(ordinal++),
- ["Int32Value"] = (Int32)dataReader.GetInt64(ordinal++),
+ ["Int16Value"] = (short)dataReader.GetInt64(ordinal++),
+ ["Int32Value"] = (int)dataReader.GetInt64(ordinal++),
["Int64Value"] = dataReader.GetInt64(ordinal++),
["SingleValue"] = dataReader.GetFloat(ordinal++),
- ["StringValue"] = dataReader.GetString(ordinal)
+ ["StringValue"] = dataReader.GetString(ordinal),
};
entities.Add(new DataRow(dictionary));
@@ -73,14 +55,19 @@ public List Query_Dynamic_Command()
[Benchmark(Baseline = false)]
[BenchmarkCategory(Query_Dynamic_Category)]
- public List Query_Dynamic_Dapper() =>
- [.. SqlMapper.Query(this.connection, "SELECT * FROM Entity")];
+ public List Query_Dynamic_Dapper() => [.. SqlMapper.Query(this.connection, "SELECT * FROM Entity")];
[Benchmark(Baseline = false)]
[BenchmarkCategory(Query_Dynamic_Category)]
- public List Query_Dynamic_DbConnectionPlus() =>
- [.. this.connection.Query("SELECT * FROM Entity")];
+ public List Query_Dynamic_DbConnectionPlus() => [.. this.connection.Query("SELECT * FROM Entity")];
- private const String Query_Dynamic_Category = "Query_Dynamic";
- private const Int32 Query_Dynamic_EntitiesPerOperation = 100;
+ [GlobalCleanup(
+ Targets = [nameof(Query_Dynamic_Command), nameof(Query_Dynamic_Dapper), nameof(Query_Dynamic_DbConnectionPlus)]
+ )]
+ public void Query_Dynamic__Cleanup() => this.connection.Dispose();
+
+ [GlobalSetup(
+ Targets = [nameof(Query_Dynamic_Command), nameof(Query_Dynamic_Dapper), nameof(Query_Dynamic_DbConnectionPlus)]
+ )]
+ public void Query_Dynamic__Setup() => this.SetupDatabase(Query_Dynamic_EntitiesPerOperation);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs
index 82c92c2..963968c 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Entities.cs
@@ -7,29 +7,8 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(Query_Entities_Command),
- nameof(Query_Entities_Dapper),
- nameof(Query_Entities_Dapper_Aot),
- nameof(Query_Entities_DbConnectionPlus)
- ]
- )]
- public void Query_Entities__Cleanup() =>
- this.connection.Dispose();
-
- [GlobalSetup(
- Targets =
- [
- nameof(Query_Entities_Command),
- nameof(Query_Entities_Dapper),
- nameof(Query_Entities_Dapper_Aot),
- nameof(Query_Entities_DbConnectionPlus)
- ]
- )]
- public void Query_Entities__Setup() =>
- this.SetupDatabase(Query_Entities_EntitiesPerOperation);
+ private const string Query_Entities_Category = "Query_Entities";
+ private const int Query_Entities_EntitiesPerOperation = 100;
[Benchmark(Baseline = true)]
[BenchmarkCategory(Query_Entities_Category)]
@@ -71,6 +50,23 @@ public List Query_Entities_Dapper_Aot() =>
public List Query_Entities_DbConnectionPlus() =>
[.. this.connection.Query("SELECT * FROM Entity")];
- private const String Query_Entities_Category = "Query_Entities";
- private const Int32 Query_Entities_EntitiesPerOperation = 100;
+ [GlobalCleanup(
+ Targets = [
+ nameof(Query_Entities_Command),
+ nameof(Query_Entities_Dapper),
+ nameof(Query_Entities_Dapper_Aot),
+ nameof(Query_Entities_DbConnectionPlus),
+ ]
+ )]
+ public void Query_Entities__Cleanup() => this.connection.Dispose();
+
+ [GlobalSetup(
+ Targets = [
+ nameof(Query_Entities_Command),
+ nameof(Query_Entities_Dapper),
+ nameof(Query_Entities_Dapper_Aot),
+ nameof(Query_Entities_DbConnectionPlus),
+ ]
+ )]
+ public void Query_Entities__Setup() => this.SetupDatabase(Query_Entities_EntitiesPerOperation);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Scalars.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Scalars.cs
index 864fc25..c3a12ce 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Scalars.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_Scalars.cs
@@ -7,33 +7,14 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(Query_Scalars_Command),
- nameof(Query_Scalars_Dapper),
- nameof(Query_Scalars_DbConnectionPlus)
- ]
- )]
- public void Query_Scalars__Cleanup() =>
- this.connection.Dispose();
-
- [GlobalSetup(
- Targets =
- [
- nameof(Query_Scalars_Command),
- nameof(Query_Scalars_Dapper),
- nameof(Query_Scalars_DbConnectionPlus)
- ]
- )]
- public void Query_Scalars__Setup() =>
- this.SetupDatabase(Query_Scalars_EntitiesPerOperation);
+ private const string Query_Scalars_Category = "Query_Scalars";
+ private const int Query_Scalars_EntitiesPerOperation = 600;
[Benchmark(Baseline = true)]
[BenchmarkCategory(Query_Scalars_Category)]
- public List Query_Scalars_Command()
+ public List Query_Scalars_Command()
{
- var result = new List();
+ var result = new List();
using var command = this.connection.CreateCommand();
@@ -51,14 +32,19 @@ public List Query_Scalars_Command()
[Benchmark(Baseline = false)]
[BenchmarkCategory(Query_Scalars_Category)]
- public List Query_Scalars_Dapper() =>
- [.. SqlMapper.Query(this.connection, "SELECT Id FROM Entity")];
+ public List Query_Scalars_Dapper() => [.. SqlMapper.Query(this.connection, "SELECT Id FROM Entity")];
[Benchmark(Baseline = false)]
[BenchmarkCategory(Query_Scalars_Category)]
- public List Query_Scalars_DbConnectionPlus() =>
- [.. this.connection.Query("SELECT Id FROM Entity")];
+ public List Query_Scalars_DbConnectionPlus() => [.. this.connection.Query("SELECT Id FROM Entity")];
- private const String Query_Scalars_Category = "Query_Scalars";
- private const Int32 Query_Scalars_EntitiesPerOperation = 600;
+ [GlobalCleanup(
+ Targets = [nameof(Query_Scalars_Command), nameof(Query_Scalars_Dapper), nameof(Query_Scalars_DbConnectionPlus)]
+ )]
+ public void Query_Scalars__Cleanup() => this.connection.Dispose();
+
+ [GlobalSetup(
+ Targets = [nameof(Query_Scalars_Command), nameof(Query_Scalars_Dapper), nameof(Query_Scalars_DbConnectionPlus)]
+ )]
+ public void Query_Scalars__Setup() => this.SetupDatabase(Query_Scalars_EntitiesPerOperation);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs
index 6ca112e..6c9f9af 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.Query_ValueTuples.cs
@@ -7,34 +7,14 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(Query_ValueTuples_Command),
- nameof(Query_ValueTuples_Dapper),
- nameof(Query_ValueTuples_DbConnectionPlus)
- ]
- )]
- public void Query_ValueTuples__Cleanup() =>
- this.connection.Dispose();
-
- [GlobalSetup(
- Targets =
- [
- nameof(Query_ValueTuples_Command),
- nameof(Query_ValueTuples_Dapper),
- nameof(Query_ValueTuples_DbConnectionPlus)
- ]
- )]
- public void Query_ValueTuples__Setup() =>
- this.SetupDatabase(Query_ValueTuples_EntitiesPerOperation);
+ private const string Query_ValueTuples_Category = "Query_ValueTuples";
+ private const int Query_ValueTuples_EntitiesPerOperation = 150;
[Benchmark(Baseline = true)]
[BenchmarkCategory(Query_ValueTuples_Category)]
- public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>
- Query_ValueTuples_Command()
+ public List<(long Id, DateTime DateTimeValue, TestEnum EnumValue, string StringValue)> Query_ValueTuples_Command()
{
- var result = new List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>();
+ var result = new List<(long Id, DateTime DateTimeValue, TestEnum EnumValue, string StringValue)>();
using var command = this.connection.CreateCommand();
@@ -59,13 +39,13 @@ public void Query_ValueTuples__Setup() =>
[Benchmark(Baseline = false)]
[BenchmarkCategory(Query_ValueTuples_Category)]
- public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>
- Query_ValueTuples_Dapper() =>
- [.. SqlMapper
- .Query<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>(
+ public List<(long Id, DateTime DateTimeValue, TestEnum EnumValue, string StringValue)> Query_ValueTuples_Dapper() =>
+ [
+ .. SqlMapper.Query<(long Id, DateTime DateTimeValue, TestEnum EnumValue, string StringValue)>(
this.connection,
"SELECT Id, DateTimeValue, EnumValue, StringValue FROM Entity"
- )];
+ ),
+ ];
// There is no Query_ValueTuples_Dapper_Aot benchmark, so this category's Native AOT group compares
// DbConnectionPlus against the raw DbCommand baseline alone. Dapper.AOT's generator does not materialize value
@@ -75,13 +55,33 @@ [.. SqlMapper
[Benchmark(Baseline = false)]
[BenchmarkCategory(Query_ValueTuples_Category)]
- public List<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>
- Query_ValueTuples_DbConnectionPlus() =>
- [.. this.connection
- .Query<(Int64 Id, DateTime DateTimeValue, TestEnum EnumValue, String StringValue)>(
+ public List<(
+ long Id,
+ DateTime DateTimeValue,
+ TestEnum EnumValue,
+ string StringValue
+ )> Query_ValueTuples_DbConnectionPlus() =>
+ [
+ .. this.connection.Query<(long Id, DateTime DateTimeValue, TestEnum EnumValue, string StringValue)>(
"SELECT Id, DateTimeValue, EnumValue, StringValue FROM Entity"
- )];
+ ),
+ ];
- private const String Query_ValueTuples_Category = "Query_ValueTuples";
- private const Int32 Query_ValueTuples_EntitiesPerOperation = 150;
+ [GlobalCleanup(
+ Targets = [
+ nameof(Query_ValueTuples_Command),
+ nameof(Query_ValueTuples_Dapper),
+ nameof(Query_ValueTuples_DbConnectionPlus),
+ ]
+ )]
+ public void Query_ValueTuples__Cleanup() => this.connection.Dispose();
+
+ [GlobalSetup(
+ Targets = [
+ nameof(Query_ValueTuples_Command),
+ nameof(Query_ValueTuples_Dapper),
+ nameof(Query_ValueTuples_DbConnectionPlus),
+ ]
+ )]
+ public void Query_ValueTuples__Setup() => this.SetupDatabase(Query_ValueTuples_EntitiesPerOperation);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs
index 762ab83..d1823aa 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ComplexObjects.cs
@@ -7,27 +7,66 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(TemporaryTable_ComplexObjects_Command),
- nameof(TemporaryTable_ComplexObjects_Dapper),
- nameof(TemporaryTable_ComplexObjects_DbConnectionPlus)
- ]
- )]
- public void TemporaryTable_ComplexObjects__Cleanup() =>
- this.connection.Dispose();
-
- [GlobalSetup(
- Targets =
- [
- nameof(TemporaryTable_ComplexObjects_Command),
- nameof(TemporaryTable_ComplexObjects_Dapper),
- nameof(TemporaryTable_ComplexObjects_DbConnectionPlus)
- ]
- )]
- public void TemporaryTable_ComplexObjects__Setup() =>
- this.SetupDatabase(TemporaryTable_ComplexObjects_EntitiesPerOperation);
+ private const string CreateTempEntitiesTableSql = """
+ CREATE TEMP TABLE Entities (
+ Id INTEGER,
+ BooleanValue INTEGER,
+ BytesValue BLOB,
+ ByteValue INTEGER,
+ CharValue TEXT,
+ DateTimeValue TEXT,
+ DecimalValue TEXT,
+ DoubleValue REAL,
+ EnumValue TEXT,
+ Int16Value INTEGER,
+ Int32Value INTEGER,
+ Int64Value INTEGER,
+ SingleValue REAL,
+ StringValue TEXT
+ )
+ """;
+
+ private const string InsertIntoTempEntities = """
+ INSERT INTO temp.Entities (
+ Id,
+ BooleanValue,
+ BytesValue,
+ ByteValue,
+ CharValue,
+ DateTimeValue,
+ DecimalValue,
+ DoubleValue,
+ EnumValue,
+ Int16Value,
+ Int32Value,
+ Int64Value,
+ SingleValue,
+ StringValue
+ )
+ VALUES (
+ @Id,
+ @BooleanValue,
+ @BytesValue,
+ @ByteValue,
+ @CharValue,
+ @DateTimeValue,
+ @DecimalValue,
+ @DoubleValue,
+ @EnumValue,
+ @Int16Value,
+ @Int32Value,
+ @Int64Value,
+ @SingleValue,
+ @StringValue
+ )
+ """;
+
+ private const string TemporaryTable_ComplexObjects_Category = "TemporaryTable_ComplexObjects";
+ private const int TemporaryTable_ComplexObjects_EntitiesPerOperation = 250;
+
+ private readonly List temporaryTable_ComplexObjects_Entities = Generate.Multiple(
+ TemporaryTable_ComplexObjects_EntitiesPerOperation
+ );
[Benchmark(Baseline = true)]
[BenchmarkCategory(TemporaryTable_ComplexObjects_Category)]
@@ -43,7 +82,7 @@ public List TemporaryTable_ComplexObjects_Command()
insertCommand.CommandText = InsertIntoTempEntities;
- var parameters = new Dictionary
+ var parameters = new Dictionary
{
{ "Id", new("Id", null) },
{ "BooleanValue", new("BooleanValue", null) },
@@ -58,7 +97,7 @@ public List TemporaryTable_ComplexObjects_Command()
{ "Int32Value", new("Int32Value", null) },
{ "Int64Value", new("Int64Value", null) },
{ "SingleValue", new("SingleValue", null) },
- { "StringValue", new("StringValue", null) }
+ { "StringValue", new("StringValue", null) },
};
insertCommand.Parameters.AddRange(parameters.Values);
@@ -124,65 +163,28 @@ public List TemporaryTable_ComplexObjects_Dapper()
[Benchmark(Baseline = false)]
[BenchmarkCategory(TemporaryTable_ComplexObjects_Category)]
public List TemporaryTable_ComplexObjects_DbConnectionPlus() =>
- [.. this.connection.Query($"SELECT * FROM {TemporaryTable(this.temporaryTable_ComplexObjects_Entities)}")];
-
- private readonly List temporaryTable_ComplexObjects_Entities =
- Generate.Multiple(TemporaryTable_ComplexObjects_EntitiesPerOperation);
-
- private const String CreateTempEntitiesTableSql = """
- CREATE TEMP TABLE Entities (
- Id INTEGER,
- BooleanValue INTEGER,
- BytesValue BLOB,
- ByteValue INTEGER,
- CharValue TEXT,
- DateTimeValue TEXT,
- DecimalValue TEXT,
- DoubleValue REAL,
- EnumValue TEXT,
- Int16Value INTEGER,
- Int32Value INTEGER,
- Int64Value INTEGER,
- SingleValue REAL,
- StringValue TEXT
- )
- """;
-
- private const String InsertIntoTempEntities = """
- INSERT INTO temp.Entities (
- Id,
- BooleanValue,
- BytesValue,
- ByteValue,
- CharValue,
- DateTimeValue,
- DecimalValue,
- DoubleValue,
- EnumValue,
- Int16Value,
- Int32Value,
- Int64Value,
- SingleValue,
- StringValue
- )
- VALUES (
- @Id,
- @BooleanValue,
- @BytesValue,
- @ByteValue,
- @CharValue,
- @DateTimeValue,
- @DecimalValue,
- @DoubleValue,
- @EnumValue,
- @Int16Value,
- @Int32Value,
- @Int64Value,
- @SingleValue,
- @StringValue
- )
- """;
-
- private const String TemporaryTable_ComplexObjects_Category = "TemporaryTable_ComplexObjects";
- private const Int32 TemporaryTable_ComplexObjects_EntitiesPerOperation = 250;
+ [
+ .. this.connection.Query(
+ $"SELECT * FROM {TemporaryTable(this.temporaryTable_ComplexObjects_Entities)}"
+ ),
+ ];
+
+ [GlobalCleanup(
+ Targets = [
+ nameof(TemporaryTable_ComplexObjects_Command),
+ nameof(TemporaryTable_ComplexObjects_Dapper),
+ nameof(TemporaryTable_ComplexObjects_DbConnectionPlus),
+ ]
+ )]
+ public void TemporaryTable_ComplexObjects__Cleanup() => this.connection.Dispose();
+
+ [GlobalSetup(
+ Targets = [
+ nameof(TemporaryTable_ComplexObjects_Command),
+ nameof(TemporaryTable_ComplexObjects_Dapper),
+ nameof(TemporaryTable_ComplexObjects_DbConnectionPlus),
+ ]
+ )]
+ public void TemporaryTable_ComplexObjects__Setup() =>
+ this.SetupDatabase(TemporaryTable_ComplexObjects_EntitiesPerOperation);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs
index 69f021a..94e3747 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.TemporaryTable_ScalarValues.cs
@@ -7,31 +7,17 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(TemporaryTable_ScalarValues_Command),
- nameof(TemporaryTable_ScalarValues_Dapper),
- nameof(TemporaryTable_ScalarValues_DbConnectionPlus)
- ]
- )]
- public void TemporaryTable_ScalarValues__Cleanup() =>
- this.connection.Dispose();
+ private const string TemporaryTable_ScalarValues_Category = "TemporaryTable_ScalarValues";
+ private const int TemporaryTable_ScalarValues_ValuesPerOperation = 5000;
- [GlobalSetup(
- Targets =
- [
- nameof(TemporaryTable_ScalarValues_Command),
- nameof(TemporaryTable_ScalarValues_Dapper),
- nameof(TemporaryTable_ScalarValues_DbConnectionPlus)
- ]
- )]
- public void TemporaryTable_ScalarValues__Setup() =>
- this.SetupDatabase(0);
+ private readonly List temporaryTable_ScalarValues_Values =
+ [
+ .. Enumerable.Range(0, TemporaryTable_ScalarValues_ValuesPerOperation).Select(a => (long)a),
+ ];
[Benchmark(Baseline = true)]
[BenchmarkCategory(TemporaryTable_ScalarValues_Category)]
- public List TemporaryTable_ScalarValues_Command()
+ public List TemporaryTable_ScalarValues_Command()
{
using var createTableCommand = this.connection.CreateCommand();
createTableCommand.CommandText = "CREATE TEMP TABLE \"Values\" (Value INTEGER)";
@@ -40,10 +26,7 @@ public List TemporaryTable_ScalarValues_Command()
using var insertCommand = this.connection.CreateCommand();
insertCommand.CommandText = "INSERT INTO temp.\"Values\" (Value) VALUES (@Value)";
- var valueParameter = new SqliteParameter
- {
- ParameterName = "@Value"
- };
+ var valueParameter = new SqliteParameter { ParameterName = "@Value" };
insertCommand.Parameters.Add(valueParameter);
@@ -60,7 +43,7 @@ public List TemporaryTable_ScalarValues_Command()
using var dataReader = selectCommand.ExecuteReader();
- var result = new List();
+ var result = new List();
while (dataReader.Read())
{
@@ -76,7 +59,7 @@ public List TemporaryTable_ScalarValues_Command()
[Benchmark(Baseline = false)]
[BenchmarkCategory(TemporaryTable_ScalarValues_Category)]
- public List TemporaryTable_ScalarValues_Dapper()
+ public List TemporaryTable_ScalarValues_Dapper()
{
SqlMapper.Execute(this.connection, "CREATE TEMP TABLE \"Values\" (Value INTEGER)");
@@ -87,7 +70,7 @@ public List TemporaryTable_ScalarValues_Dapper()
this.temporaryTable_ScalarValues_Values.Select(a => new { Value = a })
);
- var result = SqlMapper.Query(this.connection, "SELECT Value FROM temp.\"Values\"").ToList();
+ var result = SqlMapper.Query(this.connection, "SELECT Value FROM temp.\"Values\"").ToList();
SqlMapper.Execute(this.connection, "DROP TABLE temp.\"Values\"");
@@ -96,13 +79,28 @@ public List TemporaryTable_ScalarValues_Dapper()
[Benchmark(Baseline = false)]
[BenchmarkCategory(TemporaryTable_ScalarValues_Category)]
- public List TemporaryTable_ScalarValues_DbConnectionPlus() =>
- [.. this.connection.Query($"SELECT Value FROM {TemporaryTable(this.temporaryTable_ScalarValues_Values)}")];
+ public List TemporaryTable_ScalarValues_DbConnectionPlus() =>
+ [
+ .. this.connection.Query(
+ $"SELECT Value FROM {TemporaryTable(this.temporaryTable_ScalarValues_Values)}"
+ ),
+ ];
- private readonly List temporaryTable_ScalarValues_Values = [.. Enumerable
- .Range(0, TemporaryTable_ScalarValues_ValuesPerOperation)
- .Select(a => (Int64)a)];
+ [GlobalCleanup(
+ Targets = [
+ nameof(TemporaryTable_ScalarValues_Command),
+ nameof(TemporaryTable_ScalarValues_Dapper),
+ nameof(TemporaryTable_ScalarValues_DbConnectionPlus),
+ ]
+ )]
+ public void TemporaryTable_ScalarValues__Cleanup() => this.connection.Dispose();
- private const String TemporaryTable_ScalarValues_Category = "TemporaryTable_ScalarValues";
- private const Int32 TemporaryTable_ScalarValues_ValuesPerOperation = 5000;
+ [GlobalSetup(
+ Targets = [
+ nameof(TemporaryTable_ScalarValues_Command),
+ nameof(TemporaryTable_ScalarValues_Dapper),
+ nameof(TemporaryTable_ScalarValues_DbConnectionPlus),
+ ]
+ )]
+ public void TemporaryTable_ScalarValues__Setup() => this.SetupDatabase(0);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntities.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntities.cs
index 5da4dee..905027a 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntities.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntities.cs
@@ -7,46 +7,12 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(UpdateEntities_Command),
- nameof(UpdateEntities_Dapper),
- nameof(UpdateEntities_DbConnectionPlus)
- ]
- )]
- public void UpdateEntities__Cleanup() =>
- this.connection.Dispose();
-
- [GlobalSetup(
- Targets =
- [
- nameof(UpdateEntities_Command),
- nameof(UpdateEntities_Dapper),
- nameof(UpdateEntities_DbConnectionPlus)
- ]
- )]
- public void UpdateEntities__Setup()
- {
- this.SetupDatabase(UpdateEntities_EntitiesPerOperation);
+ private const string UpdateEntities_Category = "UpdateEntities";
+ private const int UpdateEntities_EntitiesPerOperation = 100;
+ private const int UpdateEntities_UpdatedEntitiesPoolSize = 8;
- // See the note on UpdateEntity__Setup: generating the updated entities inside the benchmark charged their
- // generation to all three implementations, and a single pre-generated set would make every invocation after
- // the first write the values that are already stored.
- this.updateEntities_ModifiedEntitiesPool =
- [
- .. Enumerable
- .Range(0, UpdateEntities_UpdatedEntitiesPoolSize)
- .Select(_ => Generate.UpdatesFor(this.entitiesInDb))
- ];
- }
-
- private List UpdateEntities_GetNextModifiedEntities()
- {
- this.updateEntities_ModifiedEntitiesPoolIndex = (this.updateEntities_ModifiedEntitiesPoolIndex + 1) % UpdateEntities_UpdatedEntitiesPoolSize;
-
- return this.updateEntities_ModifiedEntitiesPool[this.updateEntities_ModifiedEntitiesPoolIndex];
- }
+ private List> updateEntities_ModifiedEntitiesPool = null!;
+ private int updateEntities_ModifiedEntitiesPoolIndex;
[Benchmark(Baseline = true)]
[BenchmarkCategory(UpdateEntities_Category)]
@@ -57,24 +23,24 @@ public void UpdateEntities_Command()
using var command = this.connection.CreateCommand();
command.CommandText = """
- UPDATE Entity
- SET BooleanValue = @BooleanValue,
- BytesValue = @BytesValue,
- ByteValue = @ByteValue,
- CharValue = @CharValue,
- DateTimeValue = @DateTimeValue,
- DecimalValue = @DecimalValue,
- DoubleValue = @DoubleValue,
- EnumValue = @EnumValue,
- Int16Value = @Int16Value,
- Int32Value = @Int32Value,
- Int64Value = @Int64Value,
- SingleValue = @SingleValue,
- StringValue = @StringValue
- WHERE Id = @Id
- """;
-
- var parameters = new Dictionary
+ UPDATE Entity
+ SET BooleanValue = @BooleanValue,
+ BytesValue = @BytesValue,
+ ByteValue = @ByteValue,
+ CharValue = @CharValue,
+ DateTimeValue = @DateTimeValue,
+ DecimalValue = @DecimalValue,
+ DoubleValue = @DoubleValue,
+ EnumValue = @EnumValue,
+ Int16Value = @Int16Value,
+ Int32Value = @Int32Value,
+ Int64Value = @Int64Value,
+ SingleValue = @SingleValue,
+ StringValue = @StringValue
+ WHERE Id = @Id
+ """;
+
+ var parameters = new Dictionary
{
{ "Id", new("Id", null) },
{ "BooleanValue", new("BooleanValue", null) },
@@ -89,7 +55,7 @@ UPDATE Entity
{ "Int32Value", new("Int32Value", null) },
{ "Int64Value", new("Int64Value", null) },
{ "SingleValue", new("SingleValue", null) },
- { "StringValue", new("StringValue", null) }
+ { "StringValue", new("StringValue", null) },
};
command.Parameters.AddRange(parameters.Values);
@@ -112,10 +78,42 @@ public void UpdateEntities_Dapper() =>
public void UpdateEntities_DbConnectionPlus() =>
this.connection.UpdateEntities(this.UpdateEntities_GetNextModifiedEntities());
- private List> updateEntities_ModifiedEntitiesPool = null!;
- private Int32 updateEntities_ModifiedEntitiesPoolIndex;
+ [GlobalCleanup(
+ Targets = [
+ nameof(UpdateEntities_Command),
+ nameof(UpdateEntities_Dapper),
+ nameof(UpdateEntities_DbConnectionPlus),
+ ]
+ )]
+ public void UpdateEntities__Cleanup() => this.connection.Dispose();
+
+ [GlobalSetup(
+ Targets = [
+ nameof(UpdateEntities_Command),
+ nameof(UpdateEntities_Dapper),
+ nameof(UpdateEntities_DbConnectionPlus),
+ ]
+ )]
+ public void UpdateEntities__Setup()
+ {
+ this.SetupDatabase(UpdateEntities_EntitiesPerOperation);
- private const String UpdateEntities_Category = "UpdateEntities";
- private const Int32 UpdateEntities_EntitiesPerOperation = 100;
- private const Int32 UpdateEntities_UpdatedEntitiesPoolSize = 8;
+ // See the note on UpdateEntity__Setup: generating the updated entities inside the benchmark charged their
+ // generation to all three implementations, and a single pre-generated set would make every invocation after
+ // the first write the values that are already stored.
+ this.updateEntities_ModifiedEntitiesPool =
+ [
+ .. Enumerable
+ .Range(0, UpdateEntities_UpdatedEntitiesPoolSize)
+ .Select(_ => Generate.UpdatesFor(this.entitiesInDb)),
+ ];
+ }
+
+ private List UpdateEntities_GetNextModifiedEntities()
+ {
+ this.updateEntities_ModifiedEntitiesPoolIndex =
+ (this.updateEntities_ModifiedEntitiesPoolIndex + 1) % UpdateEntities_UpdatedEntitiesPoolSize;
+
+ return this.updateEntities_ModifiedEntitiesPool[this.updateEntities_ModifiedEntitiesPoolIndex];
+ }
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntity.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntity.cs
index 7ee2689..ef420a1 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntity.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.UpdateEntity.cs
@@ -7,50 +7,11 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public partial class Benchmarks
{
- [GlobalCleanup(
- Targets =
- [
- nameof(UpdateEntity_Command),
- nameof(UpdateEntity_Dapper),
- nameof(UpdateEntity_DbConnectionPlus)
- ]
- )]
- public void UpdateEntity__Cleanup() =>
- this.connection.Dispose();
+ private const string UpdateEntity_Category = "UpdateEntity";
+ private const int UpdateEntity_UpdatedEntityPoolSize = 64;
- [GlobalSetup(
- Targets =
- [
- nameof(UpdateEntity_Command),
- nameof(UpdateEntity_Dapper),
- nameof(UpdateEntity_DbConnectionPlus)
- ]
- )]
- public void UpdateEntity__Setup()
- {
- this.SetupDatabase(1);
-
- // Building the updated entity inside the benchmark charged its generation to all three implementations -
- // including the DbCommand baseline and Dapper - which both dominated the measurement and hid changes in
- // DbConnectionPlus behind a baseline that moved with them.
- //
- // A pool rather than a single entity, so that consecutive invocations write different values. Reusing one
- // entity would mean every invocation after the first writes the values that are already stored, which is not
- // what an update does in practice.
- this.updateEntity_ModifiedEntitiesPool =
- [
- .. Enumerable
- .Range(0, UpdateEntity_UpdatedEntityPoolSize)
- .Select(_ => Generate.UpdateFor(this.entitiesInDb[0]))
- ];
- }
-
- private BenchmarkEntity UpdateEntity_GetNextModifiedEntity()
- {
- this.updateEntity_ModifiedEntitiesPoolIndex = (this.updateEntity_ModifiedEntitiesPoolIndex + 1) % UpdateEntity_UpdatedEntityPoolSize;
-
- return this.updateEntity_ModifiedEntitiesPool[this.updateEntity_ModifiedEntitiesPoolIndex];
- }
+ private List updateEntity_ModifiedEntitiesPool = null!;
+ private int updateEntity_ModifiedEntitiesPoolIndex;
[Benchmark(Baseline = true)]
[BenchmarkCategory(UpdateEntity_Category)]
@@ -61,24 +22,24 @@ public void UpdateEntity_Command()
using var command = this.connection.CreateCommand();
command.CommandText = """
- UPDATE Entity
- SET BooleanValue = @BooleanValue,
- BytesValue = @BytesValue,
- ByteValue = @ByteValue,
- CharValue = @CharValue,
- DateTimeValue = @DateTimeValue,
- DecimalValue = @DecimalValue,
- DoubleValue = @DoubleValue,
- EnumValue = @EnumValue,
- Int16Value = @Int16Value,
- Int32Value = @Int32Value,
- Int64Value = @Int64Value,
- SingleValue = @SingleValue,
- StringValue = @StringValue
- WHERE Id = @Id
- """;
-
- var parameters = new Dictionary
+ UPDATE Entity
+ SET BooleanValue = @BooleanValue,
+ BytesValue = @BytesValue,
+ ByteValue = @ByteValue,
+ CharValue = @CharValue,
+ DateTimeValue = @DateTimeValue,
+ DecimalValue = @DecimalValue,
+ DoubleValue = @DoubleValue,
+ EnumValue = @EnumValue,
+ Int16Value = @Int16Value,
+ Int32Value = @Int32Value,
+ Int64Value = @Int64Value,
+ SingleValue = @SingleValue,
+ StringValue = @StringValue
+ WHERE Id = @Id
+ """;
+
+ var parameters = new Dictionary
{
{ "Id", new("Id", null) },
{ "BooleanValue", new("BooleanValue", null) },
@@ -93,7 +54,7 @@ UPDATE Entity
{ "Int32Value", new("Int32Value", null) },
{ "Int64Value", new("Int64Value", null) },
{ "SingleValue", new("SingleValue", null) },
- { "StringValue", new("StringValue", null) }
+ { "StringValue", new("StringValue", null) },
};
command.Parameters.AddRange(parameters.Values);
@@ -113,9 +74,38 @@ public void UpdateEntity_Dapper() =>
public void UpdateEntity_DbConnectionPlus() =>
this.connection.UpdateEntity(this.UpdateEntity_GetNextModifiedEntity());
- private List updateEntity_ModifiedEntitiesPool = null!;
- private Int32 updateEntity_ModifiedEntitiesPoolIndex;
+ [GlobalCleanup(
+ Targets = [nameof(UpdateEntity_Command), nameof(UpdateEntity_Dapper), nameof(UpdateEntity_DbConnectionPlus)]
+ )]
+ public void UpdateEntity__Cleanup() => this.connection.Dispose();
- private const String UpdateEntity_Category = "UpdateEntity";
- private const Int32 UpdateEntity_UpdatedEntityPoolSize = 64;
+ [GlobalSetup(
+ Targets = [nameof(UpdateEntity_Command), nameof(UpdateEntity_Dapper), nameof(UpdateEntity_DbConnectionPlus)]
+ )]
+ public void UpdateEntity__Setup()
+ {
+ this.SetupDatabase(1);
+
+ // Building the updated entity inside the benchmark charged its generation to all three implementations -
+ // including the DbCommand baseline and Dapper - which both dominated the measurement and hid changes in
+ // DbConnectionPlus behind a baseline that moved with them.
+ //
+ // A pool rather than a single entity, so that consecutive invocations write different values. Reusing one
+ // entity would mean every invocation after the first writes the values that are already stored, which is not
+ // what an update does in practice.
+ this.updateEntity_ModifiedEntitiesPool =
+ [
+ .. Enumerable
+ .Range(0, UpdateEntity_UpdatedEntityPoolSize)
+ .Select(_ => Generate.UpdateFor(this.entitiesInDb[0])),
+ ];
+ }
+
+ private BenchmarkEntity UpdateEntity_GetNextModifiedEntity()
+ {
+ this.updateEntity_ModifiedEntitiesPoolIndex =
+ (this.updateEntity_ModifiedEntitiesPoolIndex + 1) % UpdateEntity_UpdatedEntityPoolSize;
+
+ return this.updateEntity_ModifiedEntitiesPool[this.updateEntity_ModifiedEntitiesPoolIndex];
+ }
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs
index d778688..3dc93f4 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs
@@ -13,8 +13,36 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
[Config(typeof(BenchmarksConfig))]
public partial class Benchmarks
{
- static Benchmarks() =>
- DbConnectionPlusConfiguration.Instance.UseSqlite();
+ /*
+ * INTEGER PRIMARY KEY makes Id an alias for the rowid, so lookups by Id are b-tree descents instead of
+ * full table scans. Without it every "WHERE Id = ?" scanned the whole table, and that scan dominated the delete,
+ * update, exists and scalar benchmarks and made their results a function of the seeded row count rather than of
+ * the code under test.
+ */
+ private const string CreateEntityTableSql = """
+ CREATE TABLE Entity
+ (
+ Id INTEGER PRIMARY KEY,
+ BooleanValue INTEGER,
+ BytesValue BLOB,
+ ByteValue INTEGER,
+ CharValue TEXT,
+ DateTimeValue TEXT,
+ DecimalValue TEXT,
+ DoubleValue REAL,
+ EnumValue TEXT,
+ Int16Value INTEGER,
+ Int32Value INTEGER,
+ Int64Value INTEGER,
+ SingleValue REAL,
+ StringValue TEXT
+ );
+ """;
+
+ private SqliteConnection connection = null!;
+ private List entitiesInDb = null!;
+
+ static Benchmarks() => DbConnectionPlusConfiguration.Instance.UseSqlite();
public Benchmarks()
{
@@ -28,26 +56,7 @@ public Benchmarks()
}
}
- private void SetupDatabase(Int32 numberOfEntities)
- {
- this.connection?.Dispose();
-
- this.connection = new("Data Source=:memory:");
- this.connection.Open();
-
- using var createEntityTableCommand = this.connection.CreateCommand();
- createEntityTableCommand.CommandText = CreateEntityTableSql;
- createEntityTableCommand.ExecuteNonQuery();
-
- using var transaction = this.connection.BeginTransaction();
-
- this.entitiesInDb = Generate.Multiple(numberOfEntities);
- this.connection.InsertEntities(this.entitiesInDb, transaction);
-
- transaction.Commit();
- }
-
- private static void PopulateEntityParameters(BenchmarkEntity entity, Dictionary parameters)
+ private static void PopulateEntityParameters(BenchmarkEntity entity, Dictionary parameters)
{
parameters["Id"].Value = entity.Id;
parameters["BooleanValue"].Value = entity.BooleanValue ? 1 : 0;
@@ -67,7 +76,7 @@ private static void PopulateEntityParameters(BenchmarkEntity entity, Dictionary<
private static BenchmarkEntity ReadEntity(IDataReader dataReader)
{
- var charBuffer = new Char[1];
+ var charBuffer = new char[1];
var ordinal = 0;
@@ -75,48 +84,40 @@ private static BenchmarkEntity ReadEntity(IDataReader dataReader)
{
Id = dataReader.GetInt64(ordinal++),
BooleanValue = dataReader.GetInt64(ordinal++) == 1,
- BytesValue = (Byte[])dataReader.GetValue(ordinal++),
+ BytesValue = (byte[])dataReader.GetValue(ordinal++),
ByteValue = dataReader.GetByte(ordinal++),
- CharValue = dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1 ? charBuffer[0] : throw new InvalidOperationException(),
+ CharValue =
+ dataReader.GetChars(ordinal++, 0, charBuffer, 0, 1) == 1
+ ? charBuffer[0]
+ : throw new InvalidOperationException(),
DateTimeValue = DateTime.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture),
- DecimalValue = Decimal.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture),
+ DecimalValue = decimal.Parse(dataReader.GetString(ordinal++), CultureInfo.InvariantCulture),
DoubleValue = dataReader.GetDouble(ordinal++),
EnumValue = Enum.Parse(dataReader.GetString(ordinal++)),
- Int16Value = (Int16)dataReader.GetInt64(ordinal++),
- Int32Value = (Int32)dataReader.GetInt64(ordinal++),
+ Int16Value = (short)dataReader.GetInt64(ordinal++),
+ Int32Value = (int)dataReader.GetInt64(ordinal++),
Int64Value = dataReader.GetInt64(ordinal++),
SingleValue = dataReader.GetFloat(ordinal++),
- StringValue = dataReader.GetString(ordinal)
+ StringValue = dataReader.GetString(ordinal),
};
}
- private SqliteConnection connection = null!;
- private List entitiesInDb = null!;
+ private void SetupDatabase(int numberOfEntities)
+ {
+ this.connection?.Dispose();
- /*
- * INTEGER PRIMARY KEY makes Id an alias for the rowid, so lookups by Id are b-tree descents instead of
- * full table scans. Without it every "WHERE Id = ?" scanned the whole table, and that scan dominated the delete,
- * update, exists and scalar benchmarks and made their results a function of the seeded row count rather than of
- * the code under test.
- */
- private const String CreateEntityTableSql =
- """
- CREATE TABLE Entity
- (
- Id INTEGER PRIMARY KEY,
- BooleanValue INTEGER,
- BytesValue BLOB,
- ByteValue INTEGER,
- CharValue TEXT,
- DateTimeValue TEXT,
- DecimalValue TEXT,
- DoubleValue REAL,
- EnumValue TEXT,
- Int16Value INTEGER,
- Int32Value INTEGER,
- Int64Value INTEGER,
- SingleValue REAL,
- StringValue TEXT
- );
- """;
+ this.connection = new("Data Source=:memory:");
+ this.connection.Open();
+
+ using var createEntityTableCommand = this.connection.CreateCommand();
+ createEntityTableCommand.CommandText = CreateEntityTableSql;
+ createEntityTableCommand.ExecuteNonQuery();
+
+ using var transaction = this.connection.BeginTransaction();
+
+ this.entitiesInDb = Generate.Multiple(numberOfEntities);
+ this.connection.InsertEntities(this.entitiesInDb, transaction);
+
+ transaction.Commit();
+ }
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs
index ab808a8..a8eff1f 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksConfig.cs
@@ -10,6 +10,11 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public class BenchmarksConfig : ManualConfig
{
+ public const string AotJobId = "AOT";
+
+ // The Job column of the summary shows these.
+ public const string JitJobId = "JIT";
+
public BenchmarksConfig()
{
this.Orderer = new BenchmarksOrderer();
@@ -37,9 +42,9 @@ public BenchmarksConfig()
}
// The settings both jobs share, so that the only difference between them is the toolchain.
- private static Job CreateJob(String id) =>
- Job.Default
- .WithId(id)
+ private static Job CreateJob(string id) =>
+ Job
+ .Default.WithId(id)
// The default adaptive warmup runs ~9 iterations, but every iteration already executes tens of
// thousands of invocations, so the tiered JIT has reached steady state before the first warmup
// iteration completes. Three is enough; the rest was pure wall time.
@@ -52,8 +57,4 @@ private static Job CreateJob(String id) =>
.WithMaxIterationCount(20)
// Since DbConnectionPlus will mostly be used in server applications, we test with server GC.
.WithGcServer(true);
-
- // The Job column of the summary shows these.
- public const String JitJobId = "JIT";
- public const String AotJobId = "AOT";
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksOrderer.cs b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksOrderer.cs
index 9431d8d..bf29bb4 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksOrderer.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/BenchmarksOrderer.cs
@@ -17,48 +17,40 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
// against AOT comparison is read from the Mean column of the two rows for the same method.
public class BenchmarksOrderer : IOrderer
{
- public Boolean SeparateLogicalGroups => true;
+ public bool SeparateLogicalGroups => true;
public IEnumerable GetExecutionOrder(
ImmutableArray benchmarksCase,
IEnumerable? order = null
- ) =>
- Sort(benchmarksCase);
+ ) => Sort(benchmarksCase);
- public String? GetHighlightGroupKey(BenchmarkCase benchmarkCase) =>
- GetLogicalGroupKey(benchmarkCase);
+ public string? GetHighlightGroupKey(BenchmarkCase benchmarkCase) => GetLogicalGroupKey(benchmarkCase);
- public String? GetLogicalGroupKey(
- ImmutableArray allBenchmarksCases,
- BenchmarkCase benchmarkCase
- ) =>
+ public string? GetLogicalGroupKey(ImmutableArray allBenchmarksCases, BenchmarkCase benchmarkCase) =>
GetLogicalGroupKey(benchmarkCase);
- public IEnumerable> GetLogicalGroupOrder(
- IEnumerable> logicalGroups,
+ public IEnumerable> GetLogicalGroupOrder(
+ IEnumerable> logicalGroups,
IEnumerable? order = null
) =>
logicalGroups
.OrderBy(it => it.First().Descriptor.Categories[0], StringComparer.Ordinal)
.ThenBy(it => GetJobRank(it.First()));
- public IEnumerable GetSummaryOrder(
- ImmutableArray benchmarksCases,
- Summary summary
- ) =>
+ public IEnumerable GetSummaryOrder(ImmutableArray benchmarksCases, Summary summary) =>
Sort(benchmarksCases);
+ // Ranked rather than sorted by name, so that JIT is reported before AOT instead of alphabetically.
+ private static int GetJobRank(BenchmarkCase benchmarkCase) =>
+ benchmarkCase.Job.Id.Contains(BenchmarksConfig.JitJobId, StringComparison.Ordinal) ? 0 : 1;
+
+ private static string GetLogicalGroupKey(BenchmarkCase benchmarkCase) =>
+ $"{benchmarkCase.Descriptor.Categories.FirstOrDefault()}-{benchmarkCase.Job.Id}";
+
private static IEnumerable Sort(ImmutableArray benchmarkCases) =>
benchmarkCases
.OrderBy(a => a.Descriptor.Categories[0], StringComparer.Ordinal)
.ThenBy(GetJobRank)
.ThenByDescending(a => a.Descriptor.Baseline)
.ThenBy(a => a.Descriptor.WorkloadMethod.Name, StringComparer.Ordinal);
-
- private static String GetLogicalGroupKey(BenchmarkCase benchmarkCase) =>
- $"{benchmarkCase.Descriptor.Categories.FirstOrDefault()}-{benchmarkCase.Job.Id}";
-
- // Ranked rather than sorted by name, so that JIT is reported before AOT instead of alphabetically.
- private static Int32 GetJobRank(BenchmarkCase benchmarkCase) =>
- benchmarkCase.Job.Id.Contains(BenchmarksConfig.JitJobId, StringComparison.Ordinal) ? 0 : 1;
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs
index 3d2df03..50d2822 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/Program.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/Program.cs
@@ -4,8 +4,5 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks;
public static class Program
{
- public static void Main(String[] args) =>
- BenchmarkSwitcher
- .FromAssembly(typeof(Program).Assembly)
- .Run(args);
+ public static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/TestData/BenchmarkEntity.cs b/benchmarks/DbConnectionPlus.Benchmarks/TestData/BenchmarkEntity.cs
index 7315d19..49df891 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/TestData/BenchmarkEntity.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/TestData/BenchmarkEntity.cs
@@ -9,22 +9,22 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks.TestData;
[System.ComponentModel.DataAnnotations.Schema.Table("Entity")]
public record BenchmarkEntity
{
- public Boolean BooleanValue { get; set; }
- public Byte[] BytesValue { get; set; } = null!;
- public Byte ByteValue { get; set; }
- public Char CharValue { get; set; }
+ public bool BooleanValue { get; set; }
+ public byte ByteValue { get; set; }
+ public byte[] BytesValue { get; set; } = null!;
+ public char CharValue { get; set; }
public DateTime DateTimeValue { get; set; }
- public Decimal DecimalValue { get; set; }
- public Double DoubleValue { get; set; }
+ public decimal DecimalValue { get; set; }
+ public double DoubleValue { get; set; }
public TestEnum EnumValue { get; set; }
[System.ComponentModel.DataAnnotations.Key]
- public Int64 Id { get; set; }
+ public long Id { get; set; }
- public Int16 Int16Value { get; set; }
- public Int32 Int32Value { get; set; }
- public Int64 Int64Value { get; set; }
+ public short Int16Value { get; set; }
+ public int Int32Value { get; set; }
+ public long Int64Value { get; set; }
- public Single SingleValue { get; set; }
- public String StringValue { get; set; } = null!;
+ public float SingleValue { get; set; }
+ public string StringValue { get; set; } = null!;
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/TestData/Generate.cs b/benchmarks/DbConnectionPlus.Benchmarks/TestData/Generate.cs
index a6812b6..10e2d98 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/TestData/Generate.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/TestData/Generate.cs
@@ -1,5 +1,3 @@
-using System.Globalization;
-
namespace RentADeveloper.DbConnectionPlus.Benchmarks.TestData;
// Generates the entities the benchmarks operate on. A plain seeded generator rather than the unit test
@@ -22,12 +20,61 @@ namespace RentADeveloper.DbConnectionPlus.Benchmarks.TestData;
// three for Double and Single, and alphabetic characters only for Char.
public static class Generate
{
- public static BenchmarkEntity Single() =>
- Create(NextId());
+ private static readonly char[] characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".ToCharArray();
+
+ private static readonly DateTime dateTimeBase = new(2020, 1, 1, 0, 0, 0, DateTimeKind.Local);
+
+ // Seeded, so that every process generates the same entities.
+ private static readonly Random random = new(20260813);
+
+ // Guards random. The setup is single threaded today, but a shared unsynchronized Random silently starts
+ // returning zeroes once it is not, which would be invisible in a benchmark result.
+ private static readonly Lock syncRoot = new();
+
+ private static readonly string[] words =
+ [
+ "lorem",
+ "ipsum",
+ "dolor",
+ "sit",
+ "amet",
+ "consectetur",
+ "adipiscing",
+ "elit",
+ "sed",
+ "do",
+ "eiusmod",
+ "tempor",
+ "incididunt",
+ "ut",
+ "labore",
+ "et",
+ "dolore",
+ "magna",
+ "aliqua",
+ "enim",
+ "ad",
+ "minim",
+ "veniam",
+ "quis",
+ "nostrud",
+ "exercitation",
+ "ullamco",
+ "laboris",
+ "nisi",
+ "aliquip",
+ "ex",
+ "ea",
+ "commodo",
+ ];
+
+ private static long nextId;
- public static List Multiple(Int32 numberOfEntities) =>
+ public static List Multiple(int numberOfEntities) =>
[.. Enumerable.Range(0, numberOfEntities).Select(_ => Single())];
+ public static BenchmarkEntity Single() => Create(NextId());
+
public static BenchmarkEntity UpdateFor(BenchmarkEntity entity)
{
var updatedEntity = Create(entity.Id);
@@ -42,10 +89,9 @@ public static BenchmarkEntity UpdateFor(BenchmarkEntity entity)
return updatedEntity;
}
- public static List UpdatesFor(List entities) =>
- [.. entities.Select(UpdateFor)];
+ public static List UpdatesFor(List entities) => [.. entities.Select(UpdateFor)];
- private static BenchmarkEntity Create(Int64 id)
+ private static BenchmarkEntity Create(long id)
{
lock (syncRoot)
{
@@ -54,70 +100,46 @@ private static BenchmarkEntity Create(Int64 id)
Id = id,
BooleanValue = random.Next(2) == 1,
BytesValue = NextBytes(random.Next(1, 10)),
- ByteValue = (Byte)random.Next(0, 256),
+ ByteValue = (byte)random.Next(0, 256),
CharValue = characters[random.Next(0, characters.Length)],
// Seconds precision, and a fixed base date so that the values do not depend on when the benchmarks
// are run. The span covers roughly six years.
DateTimeValue = dateTimeBase.AddSeconds(random.Next(0, 200_000_000)),
- DecimalValue = Math.Round((Decimal)(random.NextDouble() * 999.0), 10),
+ DecimalValue = Math.Round((decimal)(random.NextDouble() * 999.0), 10),
DoubleValue = Math.Round(random.NextDouble() * 999.0, 3),
EnumValue = (TestEnum)random.Next(1, 6),
- Int16Value = (Int16)random.Next(Int16.MinValue, Int16.MaxValue + 1),
- Int32Value = random.Next(Int32.MinValue, Int32.MaxValue),
+ Int16Value = (short)random.Next(short.MinValue, short.MaxValue + 1),
+ Int32Value = random.Next(int.MinValue, int.MaxValue),
Int64Value = random.NextInt64(),
- SingleValue = (Single)Math.Round(random.NextDouble() * 999.0, 3),
- StringValue = NextSentence()
+ SingleValue = (float)Math.Round(random.NextDouble() * 999.0, 3),
+ StringValue = NextSentence(),
};
}
}
- private static Int64 NextId() =>
- Interlocked.Increment(ref nextId);
-
- private static Byte[] NextBytes(Int32 count)
+ private static byte[] NextBytes(int count)
{
- var bytes = new Byte[count];
+ var bytes = new byte[count];
random.NextBytes(bytes);
return bytes;
}
- private static String NextSentence()
+ private static long NextId() => Interlocked.Increment(ref nextId);
+
+ private static string NextSentence()
{
var wordCount = random.Next(4, 9);
- var sentence = new String[wordCount];
+ var sentence = new string[wordCount];
for (var i = 0; i < wordCount; i++)
{
sentence[i] = words[random.Next(0, words.Length)];
}
- sentence[0] = String.Concat(
- sentence[0][..1].ToUpper(CultureInfo.InvariantCulture),
- sentence[0].AsSpan(1)
- );
+ sentence[0] = string.Concat(sentence[0][..1].ToUpper(CultureInfo.InvariantCulture), sentence[0].AsSpan(1));
- return String.Join(' ', sentence) + '.';
+ return string.Join(' ', sentence) + '.';
}
-
- private static Int64 nextId;
-
- // Seeded, so that every process generates the same entities.
- private static readonly Random random = new(20260813);
-
- // Guards random. The setup is single threaded today, but a shared unsynchronized Random silently starts
- // returning zeroes once it is not, which would be invisible in a benchmark result.
- private static readonly Lock syncRoot = new();
-
- private static readonly Char[] characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".ToCharArray();
-
- private static readonly String[] words =
- [
- "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", "sed", "do", "eiusmod",
- "tempor", "incididunt", "ut", "labore", "et", "dolore", "magna", "aliqua", "enim", "ad", "minim", "veniam",
- "quis", "nostrud", "exercitation", "ullamco", "laboris", "nisi", "aliquip", "ex", "ea", "commodo"
- ];
-
- private static readonly DateTime dateTimeBase = new(2020, 1, 1, 0, 0, 0, DateTimeKind.Local);
}
diff --git a/benchmarks/DbConnectionPlus.Benchmarks/TestData/TestEnum.cs b/benchmarks/DbConnectionPlus.Benchmarks/TestData/TestEnum.cs
index 1677b51..619ac04 100644
--- a/benchmarks/DbConnectionPlus.Benchmarks/TestData/TestEnum.cs
+++ b/benchmarks/DbConnectionPlus.Benchmarks/TestData/TestEnum.cs
@@ -6,5 +6,5 @@ public enum TestEnum
Value2 = 2,
Value3 = 3,
Value4 = 4,
- Value5 = 5
+ Value5 = 5,
}
diff --git a/scripts/format-cs.ps1 b/scripts/format-cs.ps1
deleted file mode 100644
index 5765545..0000000
--- a/scripts/format-cs.ps1
+++ /dev/null
@@ -1,123 +0,0 @@
-<#
-.SYNOPSIS
- Formats C# files against .editorconfig, the way this repository's build expects them.
-
-.DESCRIPTION
- This repo builds with TreatWarningsAsErrors=true, AnalysisLevel=latest-all, EnforceCodeStyleInBuild=true,
- and .editorconfig puts csharp_style_expression_bodied_* at `error` severity. A style slip is therefore a
- hard build break, and fixing it at edit time is much cheaper than discovering it at build time.
-
- AI agents call this from a PostToolUse hook after every .cs edit - Claude Code through
- .claude/hooks/format-cs.ps1 and Codex through .codex/hooks/format-cs.ps1. A human runs it before
- building. Keep the logic here rather than in a hook, so all three run the same thing.
-
-.PARAMETER Path
- One or more .cs files to format. With no Path, every .cs file git reports as changed (tracked
- modifications plus untracked files) is formatted.
-
-.PARAMETER Scope
- How much dotnet format does:
- style ~4s - IDExxxx code-style rules, incl. the expression-bodied ones. Default.
- all ~9s - whitespace + style + 3rd-party analyzers (Roslynator, ErrorProne.NET).
- whitespace ~2s - indentation and spacing only.
-
-.EXAMPLE
- pwsh -File scripts/format-cs.ps1
-
-.EXAMPLE
- pwsh -File scripts/format-cs.ps1 src/DbConnectionPlus/Entities/EntityHelper.cs
-#>
-[CmdletBinding()]
-param(
- [Parameter(Position = 0, ValueFromRemainingArguments = $true)]
- [String[]] $Path,
-
- [ValidateSet('style', 'all', 'whitespace')]
- [String] $Scope = 'style'
-)
-
-$ErrorActionPreference = 'Stop'
-
-# scripts/ - the repository root is one level up.
-$repositoryRoot = Split-Path -Parent $PSScriptRoot
-
-function Get-ChangedCSharpFile {
- param([String] $RepositoryRoot)
-
- # 2>$null: git warns about CRLF normalization per file, which is noise here.
- $tracked = & git -C $RepositoryRoot diff --name-only HEAD -- '*.cs' 2>$null
- $untracked = & git -C $RepositoryRoot ls-files --others --exclude-standard -- '*.cs' 2>$null
-
- return @($tracked) + @($untracked) |
- Where-Object { $_ } |
- ForEach-Object { Join-Path $RepositoryRoot $_ }
-}
-
-function Get-OwningProject {
- param([String] $FilePath)
-
- $directory = Split-Path -Parent $FilePath
- while ($directory) {
- $candidate = Get-ChildItem -LiteralPath $directory -Filter '*.csproj' -File -ErrorAction SilentlyContinue |
- Select-Object -First 1
- if ($candidate) { return $candidate.FullName }
- $directory = Split-Path -Parent $directory
- }
-
- return $null
-}
-
-if (-not $Path -or $Path.Count -eq 0) {
- $Path = Get-ChangedCSharpFile -RepositoryRoot $repositoryRoot
-}
-
-$files = @($Path) |
- Where-Object { $_ } |
- Where-Object { [System.IO.Path]::GetExtension($_) -eq '.cs' } |
- Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
- ForEach-Object { (Resolve-Path -LiteralPath $_).Path } |
- # Generated and build output are not ours to format.
- Where-Object { $_ -notmatch '[\\/](bin|obj)[\\/]' } |
- Select-Object -Unique
-
-if ($files.Count -eq 0) {
- Write-Output 'format-cs: nothing to format.'
- exit 0
-}
-
-$failed = $false
-
-# One dotnet format invocation per owning project, so MSBuild loads one project rather than the solution.
-$files | Group-Object { Get-OwningProject -FilePath $_ } | ForEach-Object {
- $project = $_.Name
- if ([String]::IsNullOrWhiteSpace($project) -or -not (Test-Path -LiteralPath $project)) {
- Write-Output "format-cs: no owning .csproj for $($_.Group -join ', ') - skipped."
- return
- }
-
- $projectDirectory = Split-Path -Parent $project
-
- # `--include` matches RELATIVE paths only. Handed an absolute path it matches nothing, reports success
- # and formats nothing - a silent no-op that looks exactly like a clean file. So run from the project
- # directory and pass each file relative to it.
- $relativePaths = $_.Group | ForEach-Object { [System.IO.Path]::GetRelativePath($projectDirectory, $_) }
-
- $arguments = @('format')
- if ($Scope -ne 'all') { $arguments += $Scope }
- $arguments += @($project, '--include') + $relativePaths + @('--no-restore', '-v', 'q')
-
- Push-Location -LiteralPath $projectDirectory
- try { $output = & dotnet @arguments 2>&1 }
- finally { Pop-Location }
-
- if ($LASTEXITCODE -ne 0) {
- $failed = $true
- Write-Output "format-cs: dotnet format failed for $(Split-Path -Leaf $project):`n$output"
- }
- else {
- Write-Output "format-cs: formatted $($relativePaths.Count) file(s) in $(Split-Path -Leaf $project)."
- }
-}
-
-if ($failed) { exit 1 }
-exit 0
diff --git a/scripts/preflight.ps1 b/scripts/preflight.ps1
index b28e95e..dfcc322 100644
--- a/scripts/preflight.ps1
+++ b/scripts/preflight.ps1
@@ -1,6 +1,7 @@
<#
.SYNOPSIS
- The pre-commit gate: repo-hygiene checks, a Release build, and the unit test suite.
+ The pre-commit gate: repo-hygiene checks, style/formatting/ordering, a Release build, and the unit
+ test suite.
.DESCRIPTION
CONTRIBUTING.md requires that all tests pass and the build succeeds with no warnings. Because
@@ -18,6 +19,9 @@
scripts/verify-package-aot.ps1 -Pack after any change to a reflection path; nothing else in the
repository can see silent trimming damage.
+.PARAMETER SkipTidy
+ Skip applying style, formatting and member ordering. The build still fails on any of them.
+
.PARAMETER SkipBuild
Skip the Release build (implies -SkipTests).
@@ -34,6 +38,7 @@
param(
[Switch] $SkipBuild,
[Switch] $SkipTests,
+ [Switch] $SkipTidy,
[String] $Configuration = 'Release'
)
@@ -43,6 +48,7 @@ $repositoryRoot = Split-Path -Parent $PSScriptRoot
$solution = Join-Path $repositoryRoot 'DbConnectionPlus.slnx'
$unitTests = Join-Path $repositoryRoot 'tests/DbConnectionPlus.UnitTests/DbConnectionPlus.UnitTests.csproj'
$publicApiGuard = Join-Path $repositoryRoot 'scripts/public-api-guard.ps1'
+$tidy = Join-Path $repositoryRoot 'scripts/tidy-cs.ps1'
$failures = New-Object System.Collections.Generic.List[String]
@@ -69,7 +75,52 @@ else {
Write-Output 'Unchanged.'
}
-# --- 2. Build ----------------------------------------------------------------------------------------
+# --- 2. Style, formatting and member ordering ---------------------------------------------------------
+# Applied, not just checked. All three are build errors, so leaving them to step 3 only means a slower
+# way of finding out. The editor hooks format on every edit, but they deliberately skip the two slow
+# tools - the code-style fixers and the member reordering - and this is where those run.
+#
+# It rewrites files. That is the point, and it is why this runs before the build.
+
+if (-not $SkipTidy) {
+ Write-Section 'Style, formatting and ordering'
+
+ # Snapshot the dirty .cs files BEFORE tidying. `git diff` afterwards lists your own edits too, so
+ # reporting its count would say "tidied 40 files" when the tools touched one of them. What the run
+ # actually changed is the difference between the two lists.
+ $dirtyBefore = @(& git -C $repositoryRoot diff --name-only -- '*.cs' 2>$null | Where-Object { $_ })
+
+ & pwsh -NoProfile -NonInteractive -File $tidy -Scope all
+ if ($LASTEXITCODE -ne 0) {
+ $failures.Add('tidy')
+ Write-Output 'FAIL - tidy-cs could not finish. Run `dotnet tool restore` if the tools are missing.'
+ }
+ else {
+ $dirtyAfter = @(& git -C $repositoryRoot diff --name-only -- '*.cs' 2>$null | Where-Object { $_ })
+ $tidied = @($dirtyAfter | Where-Object { $_ -notin $dirtyBefore })
+
+ Write-Output ''
+ if ($tidied) {
+ Write-Output "Tidied $($tidied.Count) file(s) that you had not already changed:"
+ $tidied | ForEach-Object { Write-Output " $_" }
+ Write-Output 'Review the diff and include it in your commit.'
+ }
+ elseif ($dirtyBefore) {
+ # Everything the tools touched was already in your diff, so there is nothing new to point at -
+ # but the tools may still have rewritten those files, and that is worth one line.
+ Write-Output "Tidy ran clean. Your $($dirtyBefore.Count) changed .cs file(s) may have been rewritten - review the diff."
+ }
+ else {
+ Write-Output 'Already tidy.'
+ }
+ }
+}
+else {
+ Write-Section 'Style, formatting and ordering'
+ Write-Output 'Skipped (-SkipTidy).'
+}
+
+# --- 3. Build ----------------------------------------------------------------------------------------
if (-not $SkipBuild) {
Write-Section "Build ($Configuration)"
@@ -86,7 +137,7 @@ else {
Write-Output 'Skipped (-SkipBuild).'
}
-# --- 3. Unit tests -----------------------------------------------------------------------------------
+# --- 4. Unit tests -----------------------------------------------------------------------------------
if (-not $SkipBuild -and -not $SkipTests -and -not $failures.Contains('build')) {
Write-Section 'Unit tests'
diff --git a/scripts/tidy-cs.ps1 b/scripts/tidy-cs.ps1
new file mode 100644
index 0000000..8043eb6
--- /dev/null
+++ b/scripts/tidy-cs.ps1
@@ -0,0 +1,312 @@
+<#
+.SYNOPSIS
+ Applies this repository's C# style, formatting and member ordering.
+
+.DESCRIPTION
+ Three concerns, three tools, no overlap between them:
+
+ formatting whitespace, line breaks, wrapping CSharpier
+ style var, =>, this., null checks, usings Roslyn analyzers, via `dotnet format style`
+ ordering the order of types and their members ReSharper, via `jb cleanupcode`
+
+ They run in that reverse order - style, then ordering, then formatting - because each one leaves
+ whitespace behind for the next. CSharpier is always last and always has the final say.
+
+ The build enforces all three, in the tests and benchmarks as much as in the libraries:
+ EnforceCodeStyleInBuild with TreatWarningsAsErrors makes a style slip or a misplaced member an
+ error, and CSharpier.MsBuild does the same for an unformatted file. Running this first is much
+ cheaper than finding out at build time.
+
+ Needs the local tools: run `dotnet tool restore` once per clone.
+
+.PARAMETER Path
+ One or more .cs files. With no Path, every .cs file git reports as changed - tracked modifications
+ plus untracked files. Ignored by -Scope all, which always covers the whole solution.
+
+.PARAMETER Scope
+ How much runs:
+
+ format ~1s CSharpier only. The default, and what the editor hooks use.
+ style ~15s + the Roslyn code-style fixers.
+ all ~3min + member reordering. Whole solution only - ReSharper loads all of it either way.
+ This is the one to run before committing; scripts/preflight.ps1 does it for you.
+
+.PARAMETER Check
+ Report violations instead of fixing them, and exit non-zero if there are any. This is what CI runs.
+
+ -Check -Scope all is the exception: it still WRITES. ReSharper has no check mode, and neither it nor
+ CSharpier is idempotent alone - cleanupcode re-indents the content of raw string literals and
+ CSharpier puts it back - so the only honest question is whether tidying the whole tree changes it.
+ That is what -Check -Scope all asks: it tidies for real, then compares. Do not point it at a working
+ tree you are not ready to have tidied.
+
+.EXAMPLE
+ pwsh -File scripts/tidy-cs.ps1
+
+.EXAMPLE
+ pwsh -File scripts/tidy-cs.ps1 -Scope all
+
+.EXAMPLE
+ pwsh -File scripts/tidy-cs.ps1 src/DbConnectionPlus/Entities/EntityHelper.cs
+#>
+[CmdletBinding()]
+param(
+ [Parameter(Position = 0, ValueFromRemainingArguments = $true)]
+ [String[]] $Path,
+
+ [ValidateSet('format', 'style', 'all')]
+ [String] $Scope = 'format',
+
+ [Switch] $Check
+)
+
+$ErrorActionPreference = 'Stop'
+
+# scripts/ - the repository root is one level up.
+$repositoryRoot = Split-Path -Parent $PSScriptRoot
+$solution = Join-Path $repositoryRoot 'DbConnectionPlus.slnx'
+
+function Get-ChangedCSharpFile {
+ # 2>$null: git warns about CRLF normalization per file, which is noise here.
+ $tracked = & git -C $repositoryRoot diff --name-only HEAD -- '*.cs' 2>$null
+ $untracked = & git -C $repositoryRoot ls-files --others --exclude-standard -- '*.cs' 2>$null
+
+ return @($tracked) + @($untracked) |
+ Where-Object { $_ } |
+ ForEach-Object { Join-Path $repositoryRoot $_ }
+}
+
+function Get-OwningProject {
+ param([String] $FilePath)
+
+ $directory = Split-Path -Parent $FilePath
+ while ($directory) {
+ $candidate = Get-ChildItem -LiteralPath $directory -Filter '*.csproj' -File -ErrorAction SilentlyContinue |
+ Select-Object -First 1
+ if ($candidate) { return $candidate.FullName }
+ $directory = Split-Path -Parent $directory
+ }
+
+ return $null
+}
+
+function Resolve-TargetFile {
+ param([String[]] $Candidates)
+
+ return @($Candidates) |
+ Where-Object { $_ } |
+ Where-Object { [System.IO.Path]::GetExtension($_) -eq '.cs' } |
+ Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
+ ForEach-Object { (Resolve-Path -LiteralPath $_).Path } |
+ # Generated and build output are not ours to touch.
+ Where-Object { $_ -notmatch '[\\/](bin|obj)[\\/]' } |
+ Select-Object -Unique
+}
+
+$failures = New-Object System.Collections.Generic.List[String]
+
+function Invoke-Tool {
+ <#
+ Runs `dotnet ...` and returns everything it printed. The caller decides success from $LASTEXITCODE.
+
+ $ErrorActionPreference is deliberately relaxed for the call. With it at 'Stop', PowerShell turns
+ anything a native program writes to stderr into a terminating error, so a harmless warning aborts
+ the whole script with NativeCommandError. ReSharper prints one every run:
+
+ Warning: Roslyn Source Generator error from DapperInterceptorGenerator from Dapper.AOT
+ handled 1 of 1 possible call-sites ...
+
+ That is a notice, not a failure - cleanupcode still exits 0 - but it was enough to kill the script.
+ Exit codes decide success here, not stderr.
+ #>
+ param([Parameter(Mandatory)] [String[]] $Arguments)
+
+ $previous = $ErrorActionPreference
+ $ErrorActionPreference = 'Continue'
+ try { return (& dotnet @Arguments 2>&1 | Out-String) }
+ finally { $ErrorActionPreference = $previous }
+}
+
+# --- Style ----------------------------------------------------------------------------------------
+# The IDExxxx code-style rules, and the third-party analyzer fixers that have one. Whitespace rules are
+# deliberately not included: IDE0055 is off in .editorconfig, because whitespace belongs to CSharpier.
+
+function Invoke-StyleFix {
+ param([String[]] $Files, [Boolean] $VerifyOnly)
+
+ $verify = if ($VerifyOnly) { @('--verify-no-changes') } else { @() }
+
+ if (-not $Files) {
+ $arguments = @('format', 'style', $solution, '--no-restore', '-v', 'q') + $verify
+ $output = Invoke-Tool -Arguments $arguments
+ if ($LASTEXITCODE -ne 0) { $failures.Add("dotnet format style:`n$output") }
+ return
+ }
+
+ # tests/package-consumption/ is out of reach for this step, and the failure would be confusing rather
+ # than useful: those projects restore from the PACKED packages, so `dotnet format style` on one cannot
+ # even load it until `dotnet pack` has run. CSharpier still formats them - it needs no project - and the
+ # style rules there are on the author. See the code-style reference.
+ $consumers = [System.IO.Path]::Combine($repositoryRoot, 'tests', 'package-consumption')
+ $skipped = @($Files) | Where-Object { $_.StartsWith($consumers, [StringComparison]::OrdinalIgnoreCase) }
+ if ($skipped) {
+ Write-Output "tidy-cs: $($skipped.Count) file(s) under tests/package-consumption - no style pass, see AGENTS.md."
+ }
+
+ $Files = @($Files) | Where-Object { -not $_.StartsWith($consumers, [StringComparison]::OrdinalIgnoreCase) }
+ if (-not $Files) { return }
+
+ # One invocation per owning project, so MSBuild loads one project rather than the whole solution.
+ $Files | Group-Object { Get-OwningProject -FilePath $_ } | ForEach-Object {
+ $project = $_.Name
+ if ([String]::IsNullOrWhiteSpace($project) -or -not (Test-Path -LiteralPath $project)) {
+ Write-Output "tidy-cs: no owning .csproj for $($_.Group -join ', ') - skipped."
+ return
+ }
+
+ $projectDirectory = Split-Path -Parent $project
+
+ # `--include` matches RELATIVE paths only. Handed an absolute path it matches nothing, reports
+ # success and formats nothing - a silent no-op that looks exactly like a clean file. So run from
+ # the project directory and pass each file relative to it.
+ #
+ # The PROJECT has to be relative too, and that is the half that is easy to miss. Given an absolute
+ # project path, `dotnet format` reports "Formatted 0 of 0 files" and exits 0 whatever --include
+ # says, so this whole step was doing nothing at all until both halves were relative. Neither
+ # failure is visible without -v d.
+ $relativePaths = $_.Group | ForEach-Object { [System.IO.Path]::GetRelativePath($projectDirectory, $_) }
+ $projectFileName = Split-Path -Leaf $project
+
+ $arguments =
+ @('format', 'style', $projectFileName, '--include') +
+ $relativePaths +
+ @('--no-restore', '-v', 'q') +
+ $verify
+
+ Push-Location -LiteralPath $projectDirectory
+ try { $output = Invoke-Tool -Arguments $arguments }
+ finally { Pop-Location }
+
+ if ($LASTEXITCODE -ne 0) {
+ $failures.Add("dotnet format style ($(Split-Path -Leaf $project)):`n$output")
+ }
+ }
+}
+
+# --- Ordering -------------------------------------------------------------------------------------
+# ReSharper is the only tool that can reorder C# members. StyleCop reports a wrong order but cannot fix
+# one: its ElementOrderCodeFixProvider is marked [NoCodeFix] and never registered.
+#
+# The order itself is the file layout in DbConnectionPlus.slnx.DotSettings, and the ReorderMembers
+# profile in the same file enables member reordering and nothing else - no reformatting, because that
+# is CSharpier's job.
+
+function Get-CSharpFingerprint {
+ # A hash over the content of every .cs file the tools actually touch, used by -Check to tell whether
+ # tidying changed anything.
+ #
+ # The file list comes from git, not from Get-ChildItem. Walking the directory tree finds 391 .cs
+ # files where the tools see 283: build leftovers and the package consumers' NuGet cache under
+ # tests/package-consumption/.packages/ are on disk but ignored, and CSharpier honours .gitignore.
+ # Hashing those makes the check fail on files nothing tidied.
+ #
+ # `git diff` is not used either: a human running -Check usually has a dirty tree, and their own
+ # edits are not an ordering violation. Comparing before against after answers the actual question.
+ $relativePaths = & git -C $repositoryRoot ls-files --cached --others --exclude-standard -- '*.cs' 2>$null |
+ Where-Object { $_ } |
+ Sort-Object
+
+ # Fail loudly on an empty list rather than hashing nothing. git's stderr goes to $null just above, so a
+ # git that fails here returns no paths instead of an error - and two hashes of an empty stream compare
+ # equal, which would make -Check report a tidy tree without having looked at a single file. This is CI's
+ # only formatting gate; it must not be able to pass by accident.
+ if (-not $relativePaths) {
+ throw 'tidy-cs: git listed no .cs files. Is this a git repository, and is git on PATH?'
+ }
+
+ $sha = [System.Security.Cryptography.SHA256]::Create()
+ try {
+ $accumulator = New-Object System.IO.MemoryStream
+ foreach ($relativePath in $relativePaths) {
+ $fullPath = Join-Path $repositoryRoot $relativePath
+ if (-not (Test-Path -LiteralPath $fullPath -PathType Leaf)) { continue }
+
+ # The path goes into the hash too, so that adding or removing a file is a change.
+ $pathBytes = [System.Text.Encoding]::UTF8.GetBytes($relativePath)
+ $accumulator.Write($pathBytes, 0, $pathBytes.Length)
+
+ $bytes = [System.IO.File]::ReadAllBytes($fullPath)
+ $accumulator.Write($bytes, 0, $bytes.Length)
+ }
+
+ return [System.BitConverter]::ToString($sha.ComputeHash($accumulator.ToArray()))
+ }
+ finally { $sha.Dispose() }
+}
+
+function Invoke-ReorderMembers {
+ $output = Invoke-Tool -Arguments @('jb', 'cleanupcode', $solution, '--profile=ReorderMembers', '--no-build')
+ if ($LASTEXITCODE -ne 0) { $failures.Add("jb cleanupcode:`n$output") }
+}
+
+# --- Formatting -----------------------------------------------------------------------------------
+# Always last: both steps above move code around and leave whitespace that is not CSharpier's.
+
+function Invoke-Format {
+ param([String[]] $Files, [Boolean] $VerifyOnly)
+
+ # @(...) around the whole thing on purpose: an `if` writes its result to the pipeline, which
+ # enumerates a one-element array back down to a bare string. Splatting that passes "C" as the path.
+ $target = @(if ($Files) { $Files } else { $repositoryRoot })
+ $command = if ($VerifyOnly) { 'check' } else { 'format' }
+
+ $output = Invoke-Tool -Arguments (@('csharpier', $command) + $target)
+ if ($LASTEXITCODE -ne 0) { $failures.Add("csharpier $command`:`n$output") }
+}
+
+# --- Run ------------------------------------------------------------------------------------------
+
+if ($Scope -eq 'all') {
+ if ($Path) { Write-Output 'tidy-cs: -Scope all covers the whole solution; the paths given are ignored.' }
+
+ # -Check does not reach the individual tools here, because two of them are not idempotent on their
+ # own: cleanupcode re-indents the content of raw string literals and CSharpier puts it back. Asking
+ # cleanupcode alone whether it changed anything therefore always says yes. So the whole pipeline
+ # runs for real and the question is asked once, of the tree: did tidying it change anything?
+ $before = if ($Check) { Get-CSharpFingerprint } else { $null }
+
+ Invoke-StyleFix -Files @() -VerifyOnly $false
+ Invoke-ReorderMembers
+ Invoke-Format -Files @() -VerifyOnly $false
+
+ if ($Check -and -not $failures.Count -and (Get-CSharpFingerprint) -ne $before) {
+ $failures.Add('the tree is not tidy. Run: pwsh -File scripts/tidy-cs.ps1 -Scope all')
+ }
+
+ if (-not $failures.Count) {
+ Write-Output "tidy-cs: solution $(if ($Check) { 'checked' } else { 'tidied' })."
+ }
+}
+else {
+ if (-not $Path) { $Path = Get-ChangedCSharpFile }
+
+ $files = Resolve-TargetFile -Candidates $Path
+ if (-not $files) {
+ Write-Output 'tidy-cs: nothing to do.'
+ exit 0
+ }
+
+ if ($Scope -eq 'style') { Invoke-StyleFix -Files $files -VerifyOnly $Check.IsPresent }
+ Invoke-Format -Files $files -VerifyOnly $Check.IsPresent
+
+ if (-not $failures.Count) {
+ Write-Output "tidy-cs: $($files.Count) file(s) $(if ($Check) { 'checked' } else { 'tidied' })."
+ }
+}
+
+if ($failures.Count) {
+ $failures | ForEach-Object { Write-Output "tidy-cs: $_" }
+ exit 1
+}
+
+exit 0
diff --git a/src/DbConnectionPlus.DatabaseAdapters.MySql/GlobalUsings.cs b/src/DbConnectionPlus.DatabaseAdapters.MySql/GlobalUsings.cs
index cfc88dd..61f5ddc 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.MySql/GlobalUsings.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.MySql/GlobalUsings.cs
@@ -4,5 +4,4 @@
global using System.Data.Common;
global using System.Diagnostics.CodeAnalysis;
global using RentADeveloper.DbConnectionPlus.Configuration;
-global using RentADeveloper.DbConnectionPlus.DatabaseAdapters;
global using RentADeveloper.DbConnectionPlus.Entities;
diff --git a/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlConfigurationExtensions.cs b/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlConfigurationExtensions.cs
index 87d64c7..9033a63 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlConfigurationExtensions.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlConfigurationExtensions.cs
@@ -2,7 +2,9 @@
using RentADeveloper.DbConnectionPlus.DatabaseAdapters.MySql;
#pragma warning disable IDE0130
+// ReSharper disable once CheckNamespace
namespace RentADeveloper.DbConnectionPlus.Configuration;
+
#pragma warning restore IDE0130
///
diff --git a/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlDatabaseAdapter.cs b/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlDatabaseAdapter.cs
index 16d314b..6676184 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlDatabaseAdapter.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlDatabaseAdapter.cs
@@ -10,6 +10,29 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.MySql;
///
public class MySqlDatabaseAdapter : IDatabaseAdapter
{
+ private static readonly Dictionary typeToMySqlDataType = new()
+ {
+ { typeof(bool), "TINYINT(1)" },
+ { typeof(byte), "TINYINT UNSIGNED" },
+ { typeof(byte[]), "BLOB" },
+ { typeof(char), "CHAR(1)" },
+ { typeof(DateOnly), "DATE" },
+ { typeof(DateTime), "DATETIME" },
+ { typeof(decimal), "DECIMAL(65,30)" },
+ { typeof(double), "DOUBLE" },
+ { typeof(Guid), "CHAR(36)" },
+ { typeof(short), "SMALLINT" },
+ { typeof(int), "INT" },
+ { typeof(long), "BIGINT" },
+ { typeof(float), "FLOAT" },
+ { typeof(string), "TEXT" },
+ { typeof(TimeOnly), "TIME" },
+ { typeof(TimeSpan), "TIME" },
+ };
+
+ private readonly MySqlEntityManipulator entityManipulator;
+ private readonly MySqlTemporaryTableBuilder temporaryTableBuilder;
+
///
/// Initializes a new instance of the class.
///
@@ -23,11 +46,10 @@ public MySqlDatabaseAdapter()
public IEntityManipulator EntityManipulator => this.entityManipulator;
///
- public ITemporaryTableBuilder TemporaryTableBuilder =>
- this.temporaryTableBuilder;
+ public ITemporaryTableBuilder TemporaryTableBuilder => this.temporaryTableBuilder;
///
- public void BindParameterValue(DbParameter parameter, Object? value)
+ public void BindParameterValue(DbParameter parameter, object? value)
{
ArgumentNullException.ThrowIfNull(parameter);
@@ -41,16 +63,13 @@ public void BindParameterValue(DbParameter parameter, Object? value)
case Enum enumValue:
parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch
{
- EnumSerializationMode.Integers =>
- DbType.Int32,
+ EnumSerializationMode.Integers => DbType.Int32,
- EnumSerializationMode.Strings =>
- DbType.String,
+ EnumSerializationMode.Strings => DbType.String,
- _ =>
- ThrowHelper.ThrowInvalidEnumSerializationModeException(
- DbConnectionPlusConfiguration.Instance.EnumSerializationMode
- )
+ _ => ThrowHelper.ThrowInvalidEnumSerializationModeException(
+ DbConnectionPlusConfiguration.Instance.EnumSerializationMode
+ ),
};
parameter.Value = EnumSerializer.SerializeEnum(
@@ -59,7 +78,7 @@ public void BindParameterValue(DbParameter parameter, Object? value)
);
break;
- case Byte[]:
+ case byte[]:
parameter.DbType = DbType.Binary;
parameter.Value = value;
break;
@@ -71,11 +90,10 @@ public void BindParameterValue(DbParameter parameter, Object? value)
}
///
- public String FormatParameterName(String parameterName) =>
- "@" + parameterName;
+ public string FormatParameterName(string parameterName) => "@" + parameterName;
///
- public String GetDataType(Type type, EnumSerializationMode enumSerializationMode)
+ public string GetDataType(Type type, EnumSerializationMode enumSerializationMode)
{
ArgumentNullException.ThrowIfNull(type);
@@ -86,13 +104,11 @@ public String GetDataType(Type type, EnumSerializationMode enumSerializationMode
{
return enumSerializationMode switch
{
- EnumSerializationMode.Strings =>
- "VARCHAR(200)", // 200 should be enough for most enum names
+ EnumSerializationMode.Strings => "VARCHAR(200)", // 200 should be enough for most enum names
- EnumSerializationMode.Integers =>
- "INT",
+ EnumSerializationMode.Integers => "INT",
- _ => ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode)
+ _ => ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode),
};
}
@@ -109,46 +125,20 @@ public String GetDataType(Type type, EnumSerializationMode enumSerializationMode
}
///
- public String QuoteIdentifier(String identifier) =>
- "`" + identifier + "`";
+ public string QuoteIdentifier(string identifier) => "`" + identifier + "`";
///
- public String QuoteTemporaryTableName(String tableName, DbConnection connection) =>
- "`" + tableName + "`";
+ public string QuoteTemporaryTableName(string tableName, DbConnection connection) => "`" + tableName + "`";
///
- public Boolean SupportsTemporaryTables(DbConnection connection) =>
- true;
+ public bool SupportsTemporaryTables(DbConnection connection) => true;
///
- public Boolean WasSqlStatementCancelledByCancellationToken(Exception exception, CancellationToken cancellationToken)
+ public bool WasSqlStatementCancelledByCancellationToken(Exception exception, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(exception);
// MySqlConnector does not support proper statement cancellation.
return false;
}
-
- private readonly MySqlEntityManipulator entityManipulator;
- private readonly MySqlTemporaryTableBuilder temporaryTableBuilder;
-
- private static readonly Dictionary typeToMySqlDataType = new()
- {
- { typeof(Boolean), "TINYINT(1)" },
- { typeof(Byte), "TINYINT UNSIGNED" },
- { typeof(Byte[]), "BLOB" },
- { typeof(Char), "CHAR(1)" },
- { typeof(DateOnly), "DATE" },
- { typeof(DateTime), "DATETIME" },
- { typeof(Decimal), "DECIMAL(65,30)" },
- { typeof(Double), "DOUBLE" },
- { typeof(Guid), "CHAR(36)" },
- { typeof(Int16), "SMALLINT" },
- { typeof(Int32), "INT" },
- { typeof(Int64), "BIGINT" },
- { typeof(Single), "FLOAT" },
- { typeof(String), "TEXT" },
- { typeof(TimeOnly), "TIME" },
- { typeof(TimeSpan), "TIME" }
- };
}
diff --git a/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlEntityManipulator.cs b/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlEntityManipulator.cs
index 32f1126..743ce34 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlEntityManipulator.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlEntityManipulator.cs
@@ -4,28 +4,22 @@
using LinkDotNet.StringBuilder;
using RentADeveloper.DbConnectionPlus.Converters;
using RentADeveloper.DbConnectionPlus.DbCommands;
-using RentADeveloper.DbConnectionPlus.Entities;
namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.MySql;
///
/// The entity manipulator for MySQL.
///
-internal class MySqlEntityManipulator : IEntityManipulator
+/// The database adapter to use to manipulate entities.
+internal class MySqlEntityManipulator(MySqlDatabaseAdapter databaseAdapter) : IEntityManipulator
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The database adapter to use to manipulate entities.
-#pragma warning disable IDE0290 // Use primary constructor
- public MySqlEntityManipulator(MySqlDatabaseAdapter databaseAdapter) =>
- this.databaseAdapter = databaseAdapter;
-#pragma warning restore IDE0290 // Use primary constructor
+ private readonly MySqlDatabaseAdapter databaseAdapter = databaseAdapter;
+ private readonly ConcurrentDictionary entityDeleteSqlCodePerEntityType = new();
+ private readonly ConcurrentDictionary entityInsertSqlCodePerEntityType = new();
+ private readonly ConcurrentDictionary entityUpdateSqlCodePerEntityType = new();
///
- public Int32 DeleteEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int DeleteEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -72,11 +66,8 @@ CancellationToken cancellationToken
totalNumberOfAffectedRows += numberOfAffectedRows;
}
}
- catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -86,9 +77,7 @@ CancellationToken cancellationToken
}
///
- public async Task DeleteEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task DeleteEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -121,8 +110,9 @@ CancellationToken cancellationToken
DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
- var numberOfAffectedRows =
- await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
+ var numberOfAffectedRows = await command
+ .ExecuteNonQueryAsync(cancellationToken)
+ .ConfigureAwait(false);
if (numberOfAffectedRows != 1)
{
@@ -136,11 +126,8 @@ CancellationToken cancellationToken
totalNumberOfAffectedRows += numberOfAffectedRows;
}
}
- catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -150,9 +137,7 @@ CancellationToken cancellationToken
}
///
- public Int32 DeleteEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int DeleteEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -189,11 +174,8 @@ CancellationToken cancellationToken
return numberOfAffectedRows;
}
- catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -201,9 +183,7 @@ CancellationToken cancellationToken
}
///
- public async Task DeleteEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task DeleteEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -240,11 +220,8 @@ CancellationToken cancellationToken
return numberOfAffectedRows;
}
- catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -252,9 +229,7 @@ CancellationToken cancellationToken
}
///
- public Int32 InsertEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int InsertEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -266,11 +241,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateInsertEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateInsertEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -297,9 +268,8 @@ CancellationToken cancellationToken
totalNumberOfAffectedRows += reader.RecordsAffected;
}
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -309,9 +279,7 @@ CancellationToken cancellationToken
}
///
- public async Task InsertEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task InsertEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -323,11 +291,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateInsertEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateInsertEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -350,22 +314,18 @@ CancellationToken cancellationToken
#pragma warning disable CA2007
await using var reader = await command
- .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken).ConfigureAwait(false);
+ .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)
+ .ConfigureAwait(false);
#pragma warning restore CA2007
- await UpdateDatabaseGeneratedPropertiesAsync(
- entityTypeMetadata,
- reader,
- entity,
- cancellationToken
- ).ConfigureAwait(false);
+ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken)
+ .ConfigureAwait(false);
totalNumberOfAffectedRows += reader.RecordsAffected;
}
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -375,9 +335,7 @@ await UpdateDatabaseGeneratedPropertiesAsync(
}
///
- public Int32 InsertEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int InsertEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -389,11 +347,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateInsertEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateInsertEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -411,9 +365,8 @@ CancellationToken cancellationToken
return reader.RecordsAffected;
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -421,9 +374,7 @@ CancellationToken cancellationToken
}
///
- public async Task InsertEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task InsertEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -435,11 +386,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateInsertEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateInsertEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -453,7 +400,8 @@ CancellationToken cancellationToken
#pragma warning disable CA2007
await using var reader = await command
- .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken).ConfigureAwait(false);
+ .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)
+ .ConfigureAwait(false);
#pragma warning restore CA2007
await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken)
@@ -461,9 +409,8 @@ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity,
return reader.RecordsAffected;
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -471,9 +418,7 @@ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity,
}
///
- public Int32 UpdateEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int UpdateEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -485,11 +430,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateUpdateEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateUpdateEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -531,9 +472,8 @@ CancellationToken cancellationToken
totalNumberOfAffectedRows += reader.RecordsAffected;
}
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -543,9 +483,7 @@ CancellationToken cancellationToken
}
///
- public async Task UpdateEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task UpdateEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -557,11 +495,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateUpdateEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateUpdateEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -584,15 +518,12 @@ CancellationToken cancellationToken
#pragma warning disable CA2007
await using var reader = await command
- .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken).ConfigureAwait(false);
+ .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)
+ .ConfigureAwait(false);
#pragma warning restore CA2007
- await UpdateDatabaseGeneratedPropertiesAsync(
- entityTypeMetadata,
- reader,
- entity,
- cancellationToken
- ).ConfigureAwait(false);
+ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken)
+ .ConfigureAwait(false);
// We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it
// returns -1 when we select database generated properties via the SELECT statement after the
@@ -611,9 +542,8 @@ await UpdateDatabaseGeneratedPropertiesAsync(
totalNumberOfAffectedRows += reader.RecordsAffected;
}
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -623,9 +553,7 @@ await UpdateDatabaseGeneratedPropertiesAsync(
}
///
- public Int32 UpdateEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int UpdateEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -637,11 +565,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateUpdateEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateUpdateEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -673,9 +597,8 @@ CancellationToken cancellationToken
return reader.RecordsAffected;
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -683,9 +606,7 @@ CancellationToken cancellationToken
}
///
- public async Task UpdateEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task UpdateEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -697,11 +618,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateUpdateEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateUpdateEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -738,15 +655,89 @@ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity,
return reader.RecordsAffected;
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
}
}
+ ///
+ /// Updates the database generated properties of the provided entity from the provided data reader.
+ ///
+ /// The metadata for the entity type.
+ /// The data reader from which to read the values for the properties.
+ /// The entity to update.
+ /// A token that can be used to cancel the operation.
+ private static void UpdateDatabaseGeneratedProperties(
+ EntityTypeMetadata entityTypeMetadata,
+ DbDataReader reader,
+ object entity,
+ CancellationToken cancellationToken
+ )
+ {
+ if (entityTypeMetadata.DatabaseGeneratedProperties.Count > 0 && reader.Read())
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++)
+ {
+ var property = entityTypeMetadata.DatabaseGeneratedProperties[i];
+
+ if (!property.CanWrite)
+ {
+ continue;
+ }
+
+ var value = reader.GetValue(i);
+
+ value = ValueConverter.ConvertValueToType(value, property.PropertyType);
+
+ property.PropertySetter!(entity, value);
+ }
+ }
+ }
+
+ ///
+ /// Asynchronously updates the database generated properties of the provided entity from the provided data
+ /// reader.
+ ///
+ /// The metadata for the entity type.
+ /// The data reader from which to read the values for the properties.
+ /// The entity to update.
+ /// A token that can be used to cancel the operation.
+ /// A task that represents the asynchronous operation.
+ private static async Task UpdateDatabaseGeneratedPropertiesAsync(
+ EntityTypeMetadata entityTypeMetadata,
+ DbDataReader reader,
+ object entity,
+ CancellationToken cancellationToken
+ )
+ {
+ if (
+ entityTypeMetadata.DatabaseGeneratedProperties.Count > 0
+ && await reader.ReadAsync(cancellationToken).ConfigureAwait(false)
+ )
+ {
+ for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++)
+ {
+ var property = entityTypeMetadata.DatabaseGeneratedProperties[i];
+
+ if (!property.CanWrite)
+ {
+ continue;
+ }
+
+ var value = reader.GetValue(i);
+
+ value = ValueConverter.ConvertValueToType(value, property.PropertyType);
+
+ property.PropertySetter!(entity, value);
+ }
+ }
+ }
+
///
/// Creates a command to delete an entity.
///
@@ -772,8 +763,8 @@ EntityTypeMetadata entityTypeMetadata
var parameters = new List();
- var whereProperties = entityTypeMetadata.KeyProperties
- .Concat(entityTypeMetadata.ConcurrencyTokenProperties)
+ var whereProperties = entityTypeMetadata
+ .KeyProperties.Concat(entityTypeMetadata.ConcurrencyTokenProperties)
.Concat(entityTypeMetadata.RowVersionProperties);
foreach (var property in whereProperties)
@@ -864,7 +855,7 @@ EntityTypeMetadata entityTypeMetadata
///
/// The metadata for the entity type to delete.
/// The SQL code to delete an entity of the specified type.
- private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
+ private string GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
this.entityDeleteSqlCodePerEntityType.GetOrAdd(
entityTypeMetadata.EntityType,
_ =>
@@ -874,7 +865,7 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
ThrowHelper.ThrowEntityTypeHasNoKeyPropertyException(entityTypeMetadata.EntityType);
}
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]);
sqlBuilder.AppendLine("DELETE FROM");
@@ -888,8 +879,8 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
var prependSeparator = false;
- var whereProperties = entityTypeMetadata.KeyProperties
- .Concat(entityTypeMetadata.ConcurrencyTokenProperties)
+ var whereProperties = entityTypeMetadata
+ .KeyProperties.Concat(entityTypeMetadata.ConcurrencyTokenProperties)
.Concat(entityTypeMetadata.RowVersionProperties);
foreach (var keyProperty in whereProperties)
@@ -918,12 +909,12 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
///
/// The metadata for the entity type to insert.
/// The SQL code to insert an entity of the specified type.
- private String GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
+ private string GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
this.entityInsertSqlCodePerEntityType.GetOrAdd(
entityTypeMetadata.EntityType,
_ =>
{
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]);
sqlBuilder.Append("INSERT INTO `");
sqlBuilder.Append(entityTypeMetadata.TableName);
@@ -1049,7 +1040,7 @@ private String GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
///
/// The metadata for the entity type to update.
/// The SQL code to update an entity of the specified type.
- private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
+ private string GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
this.entityUpdateSqlCodePerEntityType.GetOrAdd(
entityTypeMetadata.EntityType,
_ =>
@@ -1059,7 +1050,7 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
ThrowHelper.ThrowEntityTypeHasNoKeyPropertyException(entityTypeMetadata.EntityType);
}
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]);
sqlBuilder.AppendLine("UPDATE");
@@ -1097,8 +1088,8 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
prependSeparator = false;
- var whereProperties = entityTypeMetadata.KeyProperties
- .Concat(entityTypeMetadata.ConcurrencyTokenProperties)
+ var whereProperties = entityTypeMetadata
+ .KeyProperties.Concat(entityTypeMetadata.ConcurrencyTokenProperties)
.Concat(entityTypeMetadata.RowVersionProperties);
foreach (var property in whereProperties)
@@ -1159,7 +1150,7 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
whereProperties =
[
.. entityTypeMetadata.KeyProperties,
- .. entityTypeMetadata.ConcurrencyTokenProperties
+ .. entityTypeMetadata.ConcurrencyTokenProperties,
];
foreach (var keyProperty in whereProperties)
@@ -1194,7 +1185,7 @@ .. entityTypeMetadata.ConcurrencyTokenProperties
private void PopulateParametersFromEntityProperties(
EntityTypeMetadata entityTypeMetadata,
List parameters,
- Object entity
+ object entity
)
{
ArgumentNullException.ThrowIfNull(parameters);
@@ -1207,84 +1198,4 @@ Object entity
this.databaseAdapter.BindParameterValue(parameter, propertyValue);
}
}
-
- ///
- /// Updates the database generated properties of the provided entity from the provided data reader.
- ///
- /// The metadata for the entity type.
- /// The data reader from which to read the values for the properties.
- /// The entity to update.
- /// A token that can be used to cancel the operation.
- private static void UpdateDatabaseGeneratedProperties(
- EntityTypeMetadata entityTypeMetadata,
- DbDataReader reader,
- Object entity,
- CancellationToken cancellationToken
- )
- {
- if (entityTypeMetadata.DatabaseGeneratedProperties.Count > 0 && reader.Read())
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++)
- {
- var property = entityTypeMetadata.DatabaseGeneratedProperties[i];
-
- if (!property.CanWrite)
- {
- continue;
- }
-
- var value = reader.GetValue(i);
-
- value = ValueConverter.ConvertValueToType(value, property.PropertyType);
-
- property.PropertySetter!(entity, value);
- }
- }
- }
-
- ///
- /// Asynchronously updates the database generated properties of the provided entity from the provided data
- /// reader.
- ///
- /// The metadata for the entity type.
- /// The data reader from which to read the values for the properties.
- /// The entity to update.
- /// A token that can be used to cancel the operation.
- /// A task that represents the asynchronous operation.
- private static async Task UpdateDatabaseGeneratedPropertiesAsync(
- EntityTypeMetadata entityTypeMetadata,
- DbDataReader reader,
- Object entity,
- CancellationToken cancellationToken
- )
- {
- if (
- entityTypeMetadata.DatabaseGeneratedProperties.Count > 0 &&
- await reader.ReadAsync(cancellationToken).ConfigureAwait(false)
- )
- {
- for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++)
- {
- var property = entityTypeMetadata.DatabaseGeneratedProperties[i];
-
- if (!property.CanWrite)
- {
- continue;
- }
-
- var value = reader.GetValue(i);
-
- value = ValueConverter.ConvertValueToType(value, property.PropertyType);
-
- property.PropertySetter!(entity, value);
- }
- }
- }
-
- private readonly MySqlDatabaseAdapter databaseAdapter;
- private readonly ConcurrentDictionary entityDeleteSqlCodePerEntityType = new();
- private readonly ConcurrentDictionary entityInsertSqlCodePerEntityType = new();
- private readonly ConcurrentDictionary entityUpdateSqlCodePerEntityType = new();
}
diff --git a/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlTemporaryTableBuilder.cs b/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlTemporaryTableBuilder.cs
index 0700f00..98c0205 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlTemporaryTableBuilder.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.MySql/MySqlTemporaryTableBuilder.cs
@@ -5,7 +5,6 @@
using MySqlConnector;
using RentADeveloper.DbConnectionPlus.Converters;
using RentADeveloper.DbConnectionPlus.DbCommands;
-using RentADeveloper.DbConnectionPlus.Entities;
using RentADeveloper.DbConnectionPlus.Extensions;
using RentADeveloper.DbConnectionPlus.Readers;
@@ -16,6 +15,8 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.MySql;
///
internal class MySqlTemporaryTableBuilder : ITemporaryTableBuilder
{
+ private readonly MySqlDatabaseAdapter databaseAdapter;
+
///
/// Initializes a new instance of the class.
///
@@ -34,10 +35,9 @@ public MySqlTemporaryTableBuilder(MySqlDatabaseAdapter databaseAdapter)
public TemporaryTableDisposer BuildTemporaryTable(
DbConnection connection,
DbTransaction? transaction,
- String name,
+ string name,
IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
CancellationToken cancellationToken = default
)
{
@@ -69,8 +69,10 @@ public TemporaryTableDisposer BuildTemporaryTable(
);
createCommand.Transaction = transaction;
- using var cancellationTokenRegistration =
- DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken);
+ using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(
+ createCommand,
+ cancellationToken
+ );
DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []);
@@ -87,8 +89,10 @@ public TemporaryTableDisposer BuildTemporaryTable(
);
createCommand.Transaction = transaction;
- using var cancellationTokenRegistration =
- DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken);
+ using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(
+ createCommand,
+ cancellationToken
+ );
DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []);
@@ -100,7 +104,7 @@ public TemporaryTableDisposer BuildTemporaryTable(
var mySqlBulkCopy = new MySqlBulkCopy(mySqlConnection, mySqlTransaction)
{
BulkCopyTimeout = 0,
- DestinationTableName = $"`{name}`"
+ DestinationTableName = $"`{name}`",
};
mySqlBulkCopy.ColumnMappings.Clear();
@@ -111,7 +115,9 @@ public TemporaryTableDisposer BuildTemporaryTable(
}
else
{
- var properties = EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead)
+ var properties = EntityHelper
+ .GetEntityTypeMetadata(valuesType)
+ .MappedProperties.Where(a => a.CanRead)
.ToList();
for (var i = 0; i < properties.Count; i++)
@@ -134,10 +140,9 @@ public TemporaryTableDisposer BuildTemporaryTable(
public async Task BuildTemporaryTableAsync(
DbConnection connection,
DbTransaction? transaction,
- String name,
+ string name,
IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
CancellationToken cancellationToken = default
)
{
@@ -173,7 +178,7 @@ public async Task BuildTemporaryTableAsync(
await using var cancellationTokenRegistration =
#pragma warning disable CA2007
- DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken);
+ DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken);
#pragma warning restore CA2007
DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []);
@@ -194,9 +199,9 @@ public async Task BuildTemporaryTableAsync(
createCommand.Transaction = transaction;
- await using var cancellationTokenRegistration =
- DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken)
- .ConfigureAwait(false);
+ await using var cancellationTokenRegistration = DbCommandHelper
+ .RegisterDbCommandCancellation(createCommand, cancellationToken)
+ .ConfigureAwait(false);
DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []);
@@ -210,7 +215,7 @@ public async Task BuildTemporaryTableAsync(
var mySqlBulkCopy = new MySqlBulkCopy(mySqlConnection, mySqlTransaction)
{
BulkCopyTimeout = 0,
- DestinationTableName = $"`{name}`"
+ DestinationTableName = $"`{name}`",
};
mySqlBulkCopy.ColumnMappings.Clear();
@@ -221,7 +226,9 @@ public async Task BuildTemporaryTableAsync(
}
else
{
- var properties = EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead)
+ var properties = EntityHelper
+ .GetEntityTypeMetadata(valuesType)
+ .MappedProperties.Where(a => a.CanRead)
.ToList();
for (var i = 0; i < properties.Count; i++)
@@ -240,89 +247,6 @@ public async Task BuildTemporaryTableAsync(
);
}
-
- ///
- /// Builds an SQL code to create a multi-column temporary table to be populated with objects of the type
- /// .
- ///
- /// The name of the table to create.
- /// The type of objects with which to populate the table.
- /// The mode to use to serialize values.
- /// The built SQL code.
- private String BuildCreateMultiColumnTemporaryTableSqlCode(
- String tableName,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type objectsType,
- EnumSerializationMode enumSerializationMode
- )
- {
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
-
- sqlBuilder.Append("CREATE TEMPORARY TABLE `");
- sqlBuilder.Append(tableName);
- sqlBuilder.AppendLine("`");
-
- sqlBuilder.Append(Constants.Indent);
- sqlBuilder.Append("(");
-
- var properties = EntityHelper.GetEntityTypeMetadata(objectsType).MappedProperties.Where(a => a.CanRead);
-
- var prependSeparator = false;
-
- foreach (var property in properties)
- {
- if (prependSeparator)
- {
- sqlBuilder.Append(", ");
- }
-
- sqlBuilder.Append("`");
- sqlBuilder.Append(property.ColumnName);
- sqlBuilder.Append("` ");
-
- var propertyType = property.PropertyType;
-
- sqlBuilder.Append(this.databaseAdapter.GetDataType(propertyType, enumSerializationMode));
-
- prependSeparator = true;
- }
-
- sqlBuilder.AppendLine(")");
-
- return sqlBuilder.ToString();
- }
-
- ///
- /// Builds an SQL code to create a single-column temporary table to be populated with values of the type
- /// .
- ///
- /// The name of the table to create.
- /// The type of values with which the table will be populated.
- /// The mode to use to serialize values.
- /// The built SQL code.
- private String BuildCreateSingleColumnTemporaryTableSqlCode(
- String tableName,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
- EnumSerializationMode enumSerializationMode
- )
- {
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[100]);
-
- sqlBuilder.Append("CREATE TEMPORARY TABLE `");
- sqlBuilder.Append(tableName);
- sqlBuilder.AppendLine("`");
-
- sqlBuilder.Append(Constants.Indent);
- sqlBuilder.Append("(`");
- sqlBuilder.Append(Constants.SingleColumnTemporaryTableColumnName);
- sqlBuilder.Append("` ");
- sqlBuilder.Append(this.databaseAdapter.GetDataType(valuesType, enumSerializationMode));
- sqlBuilder.AppendLine(")");
-
- return sqlBuilder.ToString();
- }
-
///
/// Creates a that reads data from the specified sequence of values.
///
@@ -333,14 +257,14 @@ EnumSerializationMode enumSerializationMode
///
private static EnumerableReader CreateValuesDataReader(
IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType)
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType
+ )
{
if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType())
{
if (valuesType.IsEnumOrNullableEnumType())
{
- var enumValues = new List
private NpgsqlDbType[] GetColumnDbTypes(
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType)
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType
+ )
{
var enumSerializationMode = DbConnectionPlusConfiguration.Instance.EnumSerializationMode;
@@ -297,8 +368,10 @@ private NpgsqlDbType[] GetColumnDbTypes(
return
[
- .. EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead)
- .Select(a => this.databaseAdapter.GetDbType(a.PropertyType, enumSerializationMode))
+ .. EntityHelper
+ .GetEntityTypeMetadata(valuesType)
+ .MappedProperties.Where(a => a.CanRead)
+ .Select(a => this.databaseAdapter.GetDbType(a.PropertyType, enumSerializationMode)),
];
}
@@ -312,9 +385,8 @@ .. EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.
/// A token that can be used to cancel the operation.
private void PopulateTemporaryTable(
NpgsqlConnection connection,
- String tableName,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ string tableName,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
DbDataReader dataReader,
CancellationToken cancellationToken
)
@@ -367,9 +439,8 @@ CancellationToken cancellationToken
/// A task that represents the asynchronous operation.
private async Task PopulateTemporaryTableAsync(
NpgsqlConnection connection,
- String tableName,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ string tableName,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
DbDataReader dataReader,
CancellationToken cancellationToken
)
@@ -414,74 +485,4 @@ CancellationToken cancellationToken
await importer.CompleteAsync(cancellationToken).ConfigureAwait(false);
await importer.CloseAsync(cancellationToken).ConfigureAwait(false);
}
-
- ///
- /// Creates a that reads data from the specified sequence of values.
- ///
- /// The sequence containing the values to be read.
- /// The type of values in .
- ///
- /// A that provides access to the data in .
- ///
- private static EnumerableReader CreateValuesDataReader(
- IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType)
- {
- if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType())
- {
- return new EnumerableReader(values, valuesType, Constants.SingleColumnTemporaryTableColumnName);
- }
-
- return new EnumerableReader(
- values,
- [.. EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead)],
- EnumerableReaderOptions.None
- );
- }
-
- ///
- /// Drops the temporary table with the specified name.
- ///
- /// The name of the table to drop.
- /// The connection to use to drop the table.
- /// The transaction within to drop the table.
- private static void DropTemporaryTable(String name, NpgsqlConnection connection, NpgsqlTransaction? transaction)
- {
- using var command = connection.CreateCommand();
-
- command.CommandText = $"DROP TABLE IF EXISTS \"{name}\"";
- command.Transaction = transaction;
-
- DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
-
- command.ExecuteNonQuery();
- }
-
- ///
- /// Asynchronously drops the temporary table with the specified name.
- ///
- /// The name of the table to drop.
- /// The connection to use to drop the table.
- /// The transaction within to drop the table.
- /// A task representing the asynchronous operation.
- private static async ValueTask DropTemporaryTableAsync(
- String name,
- NpgsqlConnection connection,
- NpgsqlTransaction? transaction
- )
- {
-#pragma warning disable CA2007
- await using var command = connection.CreateCommand();
-#pragma warning restore CA2007
-
- command.CommandText = $"DROP TABLE IF EXISTS \"{name}\"";
- command.Transaction = transaction;
-
- DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
-
- await command.ExecuteNonQueryAsync().ConfigureAwait(false);
- }
-
- private readonly PostgreSqlDatabaseAdapter databaseAdapter;
}
diff --git a/src/DbConnectionPlus.DatabaseAdapters.SqlServer/GlobalUsings.cs b/src/DbConnectionPlus.DatabaseAdapters.SqlServer/GlobalUsings.cs
index 33f6f8e..e20940e 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.SqlServer/GlobalUsings.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.SqlServer/GlobalUsings.cs
@@ -5,5 +5,4 @@
global using System.Diagnostics.CodeAnalysis;
global using Microsoft.Data.SqlClient;
global using RentADeveloper.DbConnectionPlus.Configuration;
-global using RentADeveloper.DbConnectionPlus.DatabaseAdapters;
global using RentADeveloper.DbConnectionPlus.Entities;
diff --git a/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerConfigurationExtensions.cs b/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerConfigurationExtensions.cs
index 0b388f0..6d79479 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerConfigurationExtensions.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerConfigurationExtensions.cs
@@ -1,7 +1,9 @@
using RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer;
#pragma warning disable IDE0130
+// ReSharper disable once CheckNamespace
namespace RentADeveloper.DbConnectionPlus.Configuration;
+
#pragma warning restore IDE0130
///
diff --git a/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerDatabaseAdapter.cs b/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerDatabaseAdapter.cs
index 983df16..8a1900c 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerDatabaseAdapter.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerDatabaseAdapter.cs
@@ -10,6 +10,31 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer;
///
public class SqlServerDatabaseAdapter : IDatabaseAdapter
{
+ private static readonly Dictionary typeToSqlDataType = new()
+ {
+ { typeof(bool), "bit" },
+ { typeof(byte), "tinyint" },
+ { typeof(byte[]), "varbinary(max)" },
+ { typeof(char), "char(1)" },
+ { typeof(DateOnly), "date" },
+ { typeof(DateTime), "datetime2" },
+ { typeof(DateTimeOffset), "datetimeoffset" },
+ { typeof(decimal), "decimal(28,10)" },
+ { typeof(double), "float" },
+ { typeof(Guid), "uniqueidentifier" },
+ { typeof(short), "smallint" },
+ { typeof(int), "int" },
+ { typeof(long), "bigint" },
+ { typeof(object), "sql_variant" },
+ { typeof(float), "real" },
+ { typeof(string), "nvarchar(max)" },
+ { typeof(TimeOnly), "time" },
+ { typeof(TimeSpan), "time" },
+ };
+
+ private readonly SqlServerEntityManipulator entityManipulator;
+ private readonly SqlServerTemporaryTableBuilder temporaryTableBuilder;
+
///
/// Initializes a new instance of the class.
///
@@ -23,11 +48,10 @@ public SqlServerDatabaseAdapter()
public IEntityManipulator EntityManipulator => this.entityManipulator;
///
- public ITemporaryTableBuilder TemporaryTableBuilder =>
- this.temporaryTableBuilder;
+ public ITemporaryTableBuilder TemporaryTableBuilder => this.temporaryTableBuilder;
///
- public void BindParameterValue(DbParameter parameter, Object? value)
+ public void BindParameterValue(DbParameter parameter, object? value)
{
ArgumentNullException.ThrowIfNull(parameter);
@@ -41,16 +65,13 @@ public void BindParameterValue(DbParameter parameter, Object? value)
case Enum enumValue:
parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch
{
- EnumSerializationMode.Integers =>
- DbType.Int32,
+ EnumSerializationMode.Integers => DbType.Int32,
- EnumSerializationMode.Strings =>
- DbType.String,
+ EnumSerializationMode.Strings => DbType.String,
- _ =>
- ThrowHelper.ThrowInvalidEnumSerializationModeException(
- DbConnectionPlusConfiguration.Instance.EnumSerializationMode
- )
+ _ => ThrowHelper.ThrowInvalidEnumSerializationModeException(
+ DbConnectionPlusConfiguration.Instance.EnumSerializationMode
+ ),
};
parameter.Value = EnumSerializer.SerializeEnum(
@@ -59,7 +80,7 @@ public void BindParameterValue(DbParameter parameter, Object? value)
);
break;
- case Byte[]:
+ case byte[]:
parameter.DbType = DbType.Binary;
parameter.Value = value;
break;
@@ -71,11 +92,10 @@ public void BindParameterValue(DbParameter parameter, Object? value)
}
///
- public String FormatParameterName(String parameterName) =>
- "@" + parameterName;
+ public string FormatParameterName(string parameterName) => "@" + parameterName;
///
- public String GetDataType(Type type, EnumSerializationMode enumSerializationMode)
+ public string GetDataType(Type type, EnumSerializationMode enumSerializationMode)
{
ArgumentNullException.ThrowIfNull(type);
@@ -86,14 +106,11 @@ public String GetDataType(Type type, EnumSerializationMode enumSerializationMode
{
return enumSerializationMode switch
{
- EnumSerializationMode.Strings =>
- "nvarchar(200)", // 200 should be enough for most enum names
+ EnumSerializationMode.Strings => "nvarchar(200)", // 200 should be enough for most enum names
- EnumSerializationMode.Integers =>
- "int",
+ EnumSerializationMode.Integers => "int",
- _ =>
- ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode)
+ _ => ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode),
};
}
@@ -110,22 +127,16 @@ public String GetDataType(Type type, EnumSerializationMode enumSerializationMode
}
///
- public String QuoteIdentifier(String identifier) =>
- "[" + identifier + "]";
+ public string QuoteIdentifier(string identifier) => "[" + identifier + "]";
///
- public String QuoteTemporaryTableName(String tableName, DbConnection connection) =>
- "[#" + tableName + "]";
+ public string QuoteTemporaryTableName(string tableName, DbConnection connection) => "[#" + tableName + "]";
///
- public Boolean SupportsTemporaryTables(DbConnection connection) =>
- true;
+ public bool SupportsTemporaryTables(DbConnection connection) => true;
///
- public Boolean WasSqlStatementCancelledByCancellationToken(
- Exception exception,
- CancellationToken cancellationToken
- )
+ public bool WasSqlStatementCancelledByCancellationToken(Exception exception, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(exception);
@@ -154,29 +165,4 @@ CancellationToken cancellationToken
return false;
}
-
- private readonly SqlServerEntityManipulator entityManipulator;
- private readonly SqlServerTemporaryTableBuilder temporaryTableBuilder;
-
- private static readonly Dictionary typeToSqlDataType = new()
- {
- { typeof(Boolean), "bit" },
- { typeof(Byte), "tinyint" },
- { typeof(Byte[]), "varbinary(max)" },
- { typeof(Char), "char(1)" },
- { typeof(DateOnly), "date" },
- { typeof(DateTime), "datetime2" },
- { typeof(DateTimeOffset), "datetimeoffset" },
- { typeof(Decimal), "decimal(28,10)" },
- { typeof(Double), "float" },
- { typeof(Guid), "uniqueidentifier" },
- { typeof(Int16), "smallint" },
- { typeof(Int32), "int" },
- { typeof(Int64), "bigint" },
- { typeof(Object), "sql_variant" },
- { typeof(Single), "real" },
- { typeof(String), "nvarchar(max)" },
- { typeof(TimeOnly), "time" },
- { typeof(TimeSpan), "time" }
- };
}
diff --git a/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerEntityManipulator.cs b/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerEntityManipulator.cs
index 04b5897..670f3ce 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerEntityManipulator.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerEntityManipulator.cs
@@ -4,26 +4,22 @@
using LinkDotNet.StringBuilder;
using RentADeveloper.DbConnectionPlus.Converters;
using RentADeveloper.DbConnectionPlus.DbCommands;
-using RentADeveloper.DbConnectionPlus.Entities;
namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer;
///
/// The entity manipulator for SQL Server.
///
-internal class SqlServerEntityManipulator : IEntityManipulator
+/// The database adapter to use to manipulate entities.
+internal class SqlServerEntityManipulator(SqlServerDatabaseAdapter databaseAdapter) : IEntityManipulator
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The database adapter to use to manipulate entities.
- public SqlServerEntityManipulator(SqlServerDatabaseAdapter databaseAdapter) =>
- this.databaseAdapter = databaseAdapter;
+ private readonly SqlServerDatabaseAdapter databaseAdapter = databaseAdapter;
+ private readonly ConcurrentDictionary entityDeleteSqlCodePerEntityType = new();
+ private readonly ConcurrentDictionary entityInsertSqlCodePerEntityType = new();
+ private readonly ConcurrentDictionary entityUpdateSqlCodePerEntityType = new();
///
- public Int32 DeleteEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int DeleteEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -70,11 +66,8 @@ CancellationToken cancellationToken
totalNumberOfAffectedRows += numberOfAffectedRows;
}
}
- catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -84,9 +77,7 @@ CancellationToken cancellationToken
}
///
- public async Task DeleteEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task DeleteEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -119,8 +110,9 @@ CancellationToken cancellationToken
DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
- var numberOfAffectedRows =
- await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
+ var numberOfAffectedRows = await command
+ .ExecuteNonQueryAsync(cancellationToken)
+ .ConfigureAwait(false);
if (numberOfAffectedRows != 1)
{
@@ -134,11 +126,8 @@ CancellationToken cancellationToken
totalNumberOfAffectedRows += numberOfAffectedRows;
}
}
- catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -148,9 +137,7 @@ CancellationToken cancellationToken
}
///
- public Int32 DeleteEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int DeleteEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -187,11 +174,8 @@ CancellationToken cancellationToken
return numberOfAffectedRows;
}
- catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -199,9 +183,7 @@ CancellationToken cancellationToken
}
///
- public async Task DeleteEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task DeleteEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -238,11 +220,8 @@ CancellationToken cancellationToken
return numberOfAffectedRows;
}
- catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -250,9 +229,7 @@ CancellationToken cancellationToken
}
///
- public Int32 InsertEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int InsertEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -264,11 +241,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateInsertEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateInsertEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -296,9 +269,8 @@ CancellationToken cancellationToken
totalNumberOfAffectedRows += reader.RecordsAffected;
}
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -308,9 +280,7 @@ CancellationToken cancellationToken
}
///
- public async Task InsertEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task InsertEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -322,11 +292,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateInsertEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateInsertEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -349,22 +315,18 @@ CancellationToken cancellationToken
#pragma warning disable CA2007
await using var reader = await command
- .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken).ConfigureAwait(false);
+ .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)
+ .ConfigureAwait(false);
#pragma warning restore CA2007
- await UpdateDatabaseGeneratedPropertiesAsync(
- entityTypeMetadata,
- reader,
- entity,
- cancellationToken
- ).ConfigureAwait(false);
+ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken)
+ .ConfigureAwait(false);
totalNumberOfAffectedRows += reader.RecordsAffected;
}
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -374,9 +336,7 @@ await UpdateDatabaseGeneratedPropertiesAsync(
}
///
- public Int32 InsertEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int InsertEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -388,11 +348,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateInsertEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateInsertEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -410,9 +366,8 @@ CancellationToken cancellationToken
return reader.RecordsAffected;
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -420,9 +375,7 @@ CancellationToken cancellationToken
}
///
- public async Task InsertEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task InsertEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -434,11 +387,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateInsertEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateInsertEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -452,7 +401,8 @@ CancellationToken cancellationToken
#pragma warning disable CA2007
await using var reader = await command
- .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken).ConfigureAwait(false);
+ .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)
+ .ConfigureAwait(false);
#pragma warning restore CA2007
await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken)
@@ -460,9 +410,8 @@ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity,
return reader.RecordsAffected;
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -470,9 +419,7 @@ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity,
}
///
- public Int32 UpdateEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int UpdateEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -484,11 +431,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateUpdateEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateUpdateEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -529,9 +472,8 @@ CancellationToken cancellationToken
totalNumberOfAffectedRows += reader.RecordsAffected;
}
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -541,9 +483,7 @@ CancellationToken cancellationToken
}
///
- public async Task UpdateEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task UpdateEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -555,11 +495,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateUpdateEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateUpdateEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -582,15 +518,12 @@ CancellationToken cancellationToken
#pragma warning disable CA2007
await using var reader = await command
- .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken).ConfigureAwait(false);
+ .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)
+ .ConfigureAwait(false);
#pragma warning restore CA2007
- await UpdateDatabaseGeneratedPropertiesAsync(
- entityTypeMetadata,
- reader,
- entity,
- cancellationToken
- ).ConfigureAwait(false);
+ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken)
+ .ConfigureAwait(false);
// We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it
// returns -1 when we select database generated properties via the OUTPUT clause.
@@ -608,9 +541,8 @@ await UpdateDatabaseGeneratedPropertiesAsync(
totalNumberOfAffectedRows += reader.RecordsAffected;
}
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -620,9 +552,7 @@ await UpdateDatabaseGeneratedPropertiesAsync(
}
///
- public Int32 UpdateEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int UpdateEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -634,11 +564,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateUpdateEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateUpdateEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -669,9 +595,8 @@ CancellationToken cancellationToken
return reader.RecordsAffected;
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -679,9 +604,7 @@ CancellationToken cancellationToken
}
///
- public async Task UpdateEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task UpdateEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -693,11 +616,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateUpdateEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateUpdateEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -733,15 +652,89 @@ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity,
return reader.RecordsAffected;
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
}
}
+ ///
+ /// Updates the database generated properties of the provided entity from the provided data reader.
+ ///
+ /// The metadata for the entity type.
+ /// The data reader from which to read the values for the properties.
+ /// The entity to update.
+ /// A token that can be used to cancel the operation.
+ private static void UpdateDatabaseGeneratedProperties(
+ EntityTypeMetadata entityTypeMetadata,
+ DbDataReader reader,
+ object entity,
+ CancellationToken cancellationToken
+ )
+ {
+ if (entityTypeMetadata.DatabaseGeneratedProperties.Count > 0 && reader.Read())
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++)
+ {
+ var property = entityTypeMetadata.DatabaseGeneratedProperties[i];
+
+ if (!property.CanWrite)
+ {
+ continue;
+ }
+
+ var value = reader.GetValue(i);
+
+ value = ValueConverter.ConvertValueToType(value, property.PropertyType);
+
+ property.PropertySetter!(entity, value);
+ }
+ }
+ }
+
+ ///
+ /// Asynchronously updates the database generated properties of the provided entity from the provided data
+ /// reader.
+ ///
+ /// The metadata for the entity type.
+ /// The data reader from which to read the values for the properties.
+ /// The entity to update.
+ /// A token that can be used to cancel the operation.
+ /// A task that represents the asynchronous operation.
+ private static async Task UpdateDatabaseGeneratedPropertiesAsync(
+ EntityTypeMetadata entityTypeMetadata,
+ DbDataReader reader,
+ object entity,
+ CancellationToken cancellationToken
+ )
+ {
+ if (
+ entityTypeMetadata.DatabaseGeneratedProperties.Count > 0
+ && await reader.ReadAsync(cancellationToken).ConfigureAwait(false)
+ )
+ {
+ for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++)
+ {
+ var property = entityTypeMetadata.DatabaseGeneratedProperties[i];
+
+ if (!property.CanWrite)
+ {
+ continue;
+ }
+
+ var value = reader.GetValue(i);
+
+ value = ValueConverter.ConvertValueToType(value, property.PropertyType);
+
+ property.PropertySetter!(entity, value);
+ }
+ }
+ }
+
///
/// Creates a command to delete an entity.
///
@@ -767,8 +760,8 @@ EntityTypeMetadata entityTypeMetadata
var parameters = new List();
- var whereProperties = entityTypeMetadata.KeyProperties
- .Concat(entityTypeMetadata.ConcurrencyTokenProperties)
+ var whereProperties = entityTypeMetadata
+ .KeyProperties.Concat(entityTypeMetadata.ConcurrencyTokenProperties)
.Concat(entityTypeMetadata.RowVersionProperties);
foreach (var property in whereProperties)
@@ -859,7 +852,7 @@ EntityTypeMetadata entityTypeMetadata
///
/// The metadata for the entity type to delete.
/// The SQL code to delete an entity of the specified type.
- private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
+ private string GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
this.entityDeleteSqlCodePerEntityType.GetOrAdd(
entityTypeMetadata.EntityType,
_ =>
@@ -869,7 +862,7 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
ThrowHelper.ThrowEntityTypeHasNoKeyPropertyException(entityTypeMetadata.EntityType);
}
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]);
sqlBuilder.AppendLine("DELETE FROM");
@@ -884,8 +877,8 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
var prependSeparator = false;
- var whereProperties = entityTypeMetadata.KeyProperties
- .Concat(entityTypeMetadata.ConcurrencyTokenProperties)
+ var whereProperties = entityTypeMetadata
+ .KeyProperties.Concat(entityTypeMetadata.ConcurrencyTokenProperties)
.Concat(entityTypeMetadata.RowVersionProperties);
foreach (var property in whereProperties)
@@ -914,12 +907,12 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
///
/// The metadata for the entity type to insert.
/// The SQL code to insert an entity of the specified type.
- private String GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
+ private string GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
this.entityInsertSqlCodePerEntityType.GetOrAdd(
entityTypeMetadata.EntityType,
_ =>
{
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]);
sqlBuilder.Append("INSERT INTO [");
sqlBuilder.Append(entityTypeMetadata.TableName);
@@ -1001,7 +994,7 @@ private String GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
///
/// The metadata for the entity type to update.
/// The SQL code to update an entity of the specified type.
- private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
+ private string GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
this.entityUpdateSqlCodePerEntityType.GetOrAdd(
entityTypeMetadata.EntityType,
_ =>
@@ -1011,7 +1004,7 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
ThrowHelper.ThrowEntityTypeHasNoKeyPropertyException(entityTypeMetadata.EntityType);
}
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]);
sqlBuilder.AppendLine("UPDATE");
@@ -1073,8 +1066,8 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
prependSeparator = false;
- var whereProperties = entityTypeMetadata.KeyProperties
- .Concat(entityTypeMetadata.ConcurrencyTokenProperties)
+ var whereProperties = entityTypeMetadata
+ .KeyProperties.Concat(entityTypeMetadata.ConcurrencyTokenProperties)
.Concat(entityTypeMetadata.RowVersionProperties);
foreach (var property in whereProperties)
@@ -1108,7 +1101,7 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
private void PopulateParametersFromEntityProperties(
EntityTypeMetadata entityTypeMetadata,
List parameters,
- Object entity
+ object entity
)
{
ArgumentNullException.ThrowIfNull(parameters);
@@ -1121,84 +1114,4 @@ Object entity
this.databaseAdapter.BindParameterValue(parameter, propertyValue);
}
}
-
- ///
- /// Updates the database generated properties of the provided entity from the provided data reader.
- ///
- /// The metadata for the entity type.
- /// The data reader from which to read the values for the properties.
- /// The entity to update.
- /// A token that can be used to cancel the operation.
- private static void UpdateDatabaseGeneratedProperties(
- EntityTypeMetadata entityTypeMetadata,
- DbDataReader reader,
- Object entity,
- CancellationToken cancellationToken
- )
- {
- if (entityTypeMetadata.DatabaseGeneratedProperties.Count > 0 && reader.Read())
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++)
- {
- var property = entityTypeMetadata.DatabaseGeneratedProperties[i];
-
- if (!property.CanWrite)
- {
- continue;
- }
-
- var value = reader.GetValue(i);
-
- value = ValueConverter.ConvertValueToType(value, property.PropertyType);
-
- property.PropertySetter!(entity, value);
- }
- }
- }
-
- ///
- /// Asynchronously updates the database generated properties of the provided entity from the provided data
- /// reader.
- ///
- /// The metadata for the entity type.
- /// The data reader from which to read the values for the properties.
- /// The entity to update.
- /// A token that can be used to cancel the operation.
- /// A task that represents the asynchronous operation.
- private static async Task UpdateDatabaseGeneratedPropertiesAsync(
- EntityTypeMetadata entityTypeMetadata,
- DbDataReader reader,
- Object entity,
- CancellationToken cancellationToken
- )
- {
- if (
- entityTypeMetadata.DatabaseGeneratedProperties.Count > 0 &&
- await reader.ReadAsync(cancellationToken).ConfigureAwait(false)
- )
- {
- for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++)
- {
- var property = entityTypeMetadata.DatabaseGeneratedProperties[i];
-
- if (!property.CanWrite)
- {
- continue;
- }
-
- var value = reader.GetValue(i);
-
- value = ValueConverter.ConvertValueToType(value, property.PropertyType);
-
- property.PropertySetter!(entity, value);
- }
- }
- }
-
- private readonly SqlServerDatabaseAdapter databaseAdapter;
- private readonly ConcurrentDictionary entityDeleteSqlCodePerEntityType = new();
- private readonly ConcurrentDictionary entityInsertSqlCodePerEntityType = new();
- private readonly ConcurrentDictionary entityUpdateSqlCodePerEntityType = new();
}
diff --git a/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerTemporaryTableBuilder.cs b/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerTemporaryTableBuilder.cs
index 8183c12..5f6304a 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerTemporaryTableBuilder.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.SqlServer/SqlServerTemporaryTableBuilder.cs
@@ -4,7 +4,6 @@
using LinkDotNet.StringBuilder;
using Microsoft.Data;
using RentADeveloper.DbConnectionPlus.DbCommands;
-using RentADeveloper.DbConnectionPlus.Entities;
using RentADeveloper.DbConnectionPlus.Extensions;
using RentADeveloper.DbConnectionPlus.Readers;
@@ -15,6 +14,16 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer;
///
internal class SqlServerTemporaryTableBuilder : ITemporaryTableBuilder
{
+ private const string GetCurrentDatabaseCollationQuery =
+ "SELECT CONVERT (VARCHAR(256), DATABASEPROPERTYEX(DB_NAME(), 'collation'))";
+
+ private static readonly ConcurrentDictionary<
+ (string DataSource, string Database),
+ string
+ > databaseCollationPerDatabase = [];
+
+ private readonly SqlServerDatabaseAdapter databaseAdapter;
+
///
/// Initializes a new instance of the class.
///
@@ -33,10 +42,9 @@ public SqlServerTemporaryTableBuilder(SqlServerDatabaseAdapter databaseAdapter)
public TemporaryTableDisposer BuildTemporaryTable(
DbConnection connection,
DbTransaction? transaction,
- String name,
+ string name,
IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
CancellationToken cancellationToken = default
)
{
@@ -76,8 +84,10 @@ public TemporaryTableDisposer BuildTemporaryTable(
);
createCommand.Transaction = transaction;
- using var cancellationTokenRegistration =
- DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken);
+ using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(
+ createCommand,
+ cancellationToken
+ );
DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []);
@@ -95,8 +105,10 @@ public TemporaryTableDisposer BuildTemporaryTable(
);
createCommand.Transaction = transaction;
- using var cancellationTokenRegistration =
- DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken);
+ using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(
+ createCommand,
+ cancellationToken
+ );
DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []);
@@ -143,10 +155,9 @@ public TemporaryTableDisposer BuildTemporaryTable(
public async Task BuildTemporaryTableAsync(
DbConnection connection,
DbTransaction? transaction,
- String name,
+ string name,
IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
CancellationToken cancellationToken = default
)
{
@@ -188,8 +199,9 @@ public async Task BuildTemporaryTableAsync(
createCommand.Transaction = transaction;
#pragma warning restore CA2007
- await using var cancellationTokenRegistration =
- DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken).ConfigureAwait(false);
+ await using var cancellationTokenRegistration = DbCommandHelper
+ .RegisterDbCommandCancellation(createCommand, cancellationToken)
+ .ConfigureAwait(false);
DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []);
@@ -209,8 +221,9 @@ public async Task BuildTemporaryTableAsync(
);
createCommand.Transaction = transaction;
- await using var cancellationTokenRegistration =
- DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken).ConfigureAwait(false);
+ await using var cancellationTokenRegistration = DbCommandHelper
+ .RegisterDbCommandCancellation(createCommand, cancellationToken)
+ .ConfigureAwait(false);
DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []);
@@ -262,6 +275,128 @@ public async Task BuildTemporaryTableAsync(
);
}
+ ///
+ /// Creates a that reads data from the specified sequence of values.
+ ///
+ /// The sequence containing the values to be read.
+ /// The type of values in .
+ /// A that provides access to the data in .
+ private static EnumerableReader CreateValuesDataReader(
+ IEnumerable values,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType
+ )
+ {
+ if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType())
+ {
+ return new(values, valuesType, Constants.SingleColumnTemporaryTableColumnName);
+ }
+
+ return new(
+ values,
+ [.. EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead)],
+ EnumerableReaderOptions.None
+ );
+ }
+
+ ///
+ /// Drops the temporary table with the specified name.
+ ///
+ /// The name of the table to drop.
+ /// The connection to use to drop the table.
+ /// The transaction within to drop the table.
+ private static void DropTemporaryTable(string name, SqlConnection connection, SqlTransaction? transaction)
+ {
+ using var command = connection.CreateCommand();
+
+ command.CommandText = $"IF OBJECT_ID('tempdb..#{name}', 'U') IS NOT NULL DROP TABLE [#{name}]";
+ command.Transaction = transaction;
+
+ DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
+
+ command.ExecuteNonQuery();
+ }
+
+ ///
+ /// Asynchronously drops the temporary table with the specified name.
+ ///
+ /// The name of the table to drop.
+ /// The connection to use to drop the table.
+ /// The transaction within to drop the table.
+ /// A task representing the asynchronous operation.
+ private static async ValueTask DropTemporaryTableAsync(
+ string name,
+ SqlConnection connection,
+ SqlTransaction? transaction
+ )
+ {
+#pragma warning disable CA2007
+ await using var command = connection.CreateCommand();
+#pragma warning restore CA2007
+
+ command.CommandText = $"IF OBJECT_ID('tempdb..#{name}', 'U') IS NOT NULL DROP TABLE [#{name}]";
+ command.Transaction = transaction;
+
+ DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
+
+ await command.ExecuteNonQueryAsync().ConfigureAwait(false);
+ }
+
+ ///
+ /// Gets the collation of the database the specified connection is currently connected to.
+ ///
+ /// The connection to the database of which to get the collation.
+ /// The database transaction within to perform the operation.
+ /// The collation of the database the specified connection is currently connected to.
+ private static string GetCurrentDatabaseCollation(SqlConnection connection, SqlTransaction? transaction = null) =>
+ databaseCollationPerDatabase.GetOrAdd(
+ (connection.DataSource, connection.Database),
+ static (_, args) =>
+ {
+ using var command = args.connection.CreateCommand();
+
+ command.CommandText = GetCurrentDatabaseCollationQuery;
+ command.Transaction = args.transaction;
+
+ DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
+
+ return (string)command.ExecuteScalar()!;
+ },
+ (connection, transaction)
+ );
+
+ ///
+ /// Asynchronously gets the collation of the database the specified connection is currently connected to.
+ ///
+ /// The connection to the database of which to get the collation.
+ /// The database transaction within to perform the operation.
+ ///
+ /// A task representing the asynchronous operation.
+ /// will contain the collation of the database the specified connection is
+ /// currently connected to.
+ ///
+ private static async ValueTask GetCurrentDatabaseCollationAsync(
+ SqlConnection connection,
+ SqlTransaction? transaction = null
+ )
+ {
+ if (databaseCollationPerDatabase.TryGetValue((connection.DataSource, connection.Database), out var collation))
+ {
+ return collation;
+ }
+
+#pragma warning disable CA2007
+ await using var command = connection.CreateCommand();
+#pragma warning restore CA2007
+
+ command.CommandText = GetCurrentDatabaseCollationQuery;
+ command.Transaction = transaction;
+
+ DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
+
+ collation = (string)(await command.ExecuteScalarAsync().ConfigureAwait(false))!;
+
+ return databaseCollationPerDatabase.GetOrAdd((connection.DataSource, connection.Database), collation);
+ }
///
/// Builds an SQL code to create a multi-column temporary table to be populated with objects of the type
@@ -272,15 +407,14 @@ public async Task BuildTemporaryTableAsync(
/// The collation to use for text columns.
/// The mode to use to serialize values.
/// The built SQL code.
- private String BuildCreateMultiColumnTemporaryTableSqlCode(
- String tableName,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type objectsType,
- String collation,
+ private string BuildCreateMultiColumnTemporaryTableSqlCode(
+ string tableName,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type objectsType,
+ string collation,
EnumSerializationMode enumSerializationMode
)
{
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]);
sqlBuilder.Append("CREATE TABLE [#");
sqlBuilder.Append(tableName);
@@ -309,12 +443,8 @@ EnumSerializationMode enumSerializationMode
sqlBuilder.Append(this.databaseAdapter.GetDataType(propertyType, enumSerializationMode));
if (
- propertyType == typeof(String)
- ||
- (
- propertyType.IsEnumOrNullableEnumType() &&
- enumSerializationMode == EnumSerializationMode.Strings
- )
+ propertyType == typeof(string)
+ || (propertyType.IsEnumOrNullableEnumType() && enumSerializationMode == EnumSerializationMode.Strings)
)
{
sqlBuilder.Append(" COLLATE ");
@@ -339,16 +469,15 @@ EnumSerializationMode enumSerializationMode
/// The collation to use for text columns.
/// The mode to use to serialize values.
/// The built SQL code.
- private String BuildCreateSingleColumnTemporaryTableSqlCode(
- String tableName,
+ private string BuildCreateSingleColumnTemporaryTableSqlCode(
+ string tableName,
IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
- String collation,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
+ string collation,
EnumSerializationMode enumSerializationMode
)
{
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[100]);
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[100]);
sqlBuilder.Append("CREATE TABLE [#");
sqlBuilder.Append(tableName);
@@ -359,11 +488,11 @@ EnumSerializationMode enumSerializationMode
sqlBuilder.Append(Constants.SingleColumnTemporaryTableColumnName);
sqlBuilder.Append("] ");
- if (valuesType == typeof(String))
+ if (valuesType == typeof(string))
{
var maxLength = 0;
- foreach (String? value in values)
+ foreach (string? value in values)
{
if (value?.Length > maxLength)
{
@@ -394,12 +523,8 @@ EnumSerializationMode enumSerializationMode
}
if (
- valuesType == typeof(String)
- ||
- (
- valuesType.IsEnumOrNullableEnumType() &&
- enumSerializationMode == EnumSerializationMode.Strings
- )
+ valuesType == typeof(string)
+ || (valuesType.IsEnumOrNullableEnumType() && enumSerializationMode == EnumSerializationMode.Strings)
)
{
sqlBuilder.Append(" COLLATE ");
@@ -410,138 +535,4 @@ EnumSerializationMode enumSerializationMode
return sqlBuilder.ToString();
}
-
- ///
- /// Creates a that reads data from the specified sequence of values.
- ///
- /// The sequence containing the values to be read.
- /// The type of values in .
- /// A that provides access to the data in .
- private static EnumerableReader CreateValuesDataReader(
- IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType)
- {
- if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType())
- {
- return new EnumerableReader(values, valuesType, Constants.SingleColumnTemporaryTableColumnName);
- }
-
- return new EnumerableReader(
- values,
- [.. EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead)],
- EnumerableReaderOptions.None
- );
- }
-
- ///
- /// Drops the temporary table with the specified name.
- ///
- /// The name of the table to drop.
- /// The connection to use to drop the table.
- /// The transaction within to drop the table.
- private static void DropTemporaryTable(String name, SqlConnection connection, SqlTransaction? transaction)
- {
- using var command = connection.CreateCommand();
-
- command.CommandText = $"IF OBJECT_ID('tempdb..#{name}', 'U') IS NOT NULL DROP TABLE [#{name}]";
- command.Transaction = transaction;
-
- DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
-
- command.ExecuteNonQuery();
- }
-
- ///
- /// Asynchronously drops the temporary table with the specified name.
- ///
- /// The name of the table to drop.
- /// The connection to use to drop the table.
- /// The transaction within to drop the table.
- /// A task representing the asynchronous operation.
- private static async ValueTask DropTemporaryTableAsync(
- String name,
- SqlConnection connection,
- SqlTransaction? transaction
- )
- {
-#pragma warning disable CA2007
- await using var command = connection.CreateCommand();
-#pragma warning restore CA2007
-
- command.CommandText = $"IF OBJECT_ID('tempdb..#{name}', 'U') IS NOT NULL DROP TABLE [#{name}]";
- command.Transaction = transaction;
-
- DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
-
- await command.ExecuteNonQueryAsync().ConfigureAwait(false);
- }
-
- ///
- /// Gets the collation of the database the specified connection is currently connected to.
- ///
- /// The connection to the database of which to get the collation.
- /// The database transaction within to perform the operation.
- /// The collation of the database the specified connection is currently connected to.
- private static String GetCurrentDatabaseCollation(
- SqlConnection connection,
- SqlTransaction? transaction = null
- ) =>
- databaseCollationPerDatabase.GetOrAdd(
- (connection.DataSource, connection.Database),
- static (_, args) =>
- {
- using var command = args.connection.CreateCommand();
-
- command.CommandText = GetCurrentDatabaseCollationQuery;
- command.Transaction = args.transaction;
-
- DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
-
- return (String)command.ExecuteScalar()!;
- },
- (connection, transaction)
- );
-
- ///
- /// Asynchronously gets the collation of the database the specified connection is currently connected to.
- ///
- /// The connection to the database of which to get the collation.
- /// The database transaction within to perform the operation.
- ///
- /// A task representing the asynchronous operation.
- /// will contain the collation of the database the specified connection is
- /// currently connected to.
- ///
- private static async ValueTask GetCurrentDatabaseCollationAsync(
- SqlConnection connection,
- SqlTransaction? transaction = null
- )
- {
- if (databaseCollationPerDatabase.TryGetValue((connection.DataSource, connection.Database), out var collation))
- {
- return collation;
- }
-
-#pragma warning disable CA2007
- await using var command = connection.CreateCommand();
-#pragma warning restore CA2007
-
- command.CommandText = GetCurrentDatabaseCollationQuery;
- command.Transaction = transaction;
-
- DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
-
- collation = (String)(await command.ExecuteScalarAsync().ConfigureAwait(false))!;
-
- return databaseCollationPerDatabase.GetOrAdd((connection.DataSource, connection.Database), collation);
- }
-
- private readonly SqlServerDatabaseAdapter databaseAdapter;
-
- private const String GetCurrentDatabaseCollationQuery =
- "SELECT CONVERT (VARCHAR(256), DATABASEPROPERTYEX(DB_NAME(), 'collation'))";
-
- private static readonly ConcurrentDictionary<(String DataSource, String Database), String>
- databaseCollationPerDatabase = [];
}
diff --git a/src/DbConnectionPlus.DatabaseAdapters.Sqlite/GlobalUsings.cs b/src/DbConnectionPlus.DatabaseAdapters.Sqlite/GlobalUsings.cs
index cfc88dd..61f5ddc 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.Sqlite/GlobalUsings.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.Sqlite/GlobalUsings.cs
@@ -4,5 +4,4 @@
global using System.Data.Common;
global using System.Diagnostics.CodeAnalysis;
global using RentADeveloper.DbConnectionPlus.Configuration;
-global using RentADeveloper.DbConnectionPlus.DatabaseAdapters;
global using RentADeveloper.DbConnectionPlus.Entities;
diff --git a/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteConfigurationExtensions.cs b/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteConfigurationExtensions.cs
index 562d8cc..faaa029 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteConfigurationExtensions.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteConfigurationExtensions.cs
@@ -2,7 +2,9 @@
using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Sqlite;
#pragma warning disable IDE0130
+// ReSharper disable once CheckNamespace
namespace RentADeveloper.DbConnectionPlus.Configuration;
+
#pragma warning restore IDE0130
///
diff --git a/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteDatabaseAdapter.cs b/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteDatabaseAdapter.cs
index 00b0a48..247210c 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteDatabaseAdapter.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteDatabaseAdapter.cs
@@ -10,6 +10,30 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.Sqlite;
///
public class SqliteDatabaseAdapter : IDatabaseAdapter
{
+ private static readonly Dictionary typeToSqliteDataType = new()
+ {
+ { typeof(bool), "INTEGER" },
+ { typeof(byte), "INTEGER" },
+ { typeof(byte[]), "BLOB" },
+ { typeof(char), "TEXT" },
+ { typeof(DateOnly), "TEXT" },
+ { typeof(DateTime), "TEXT" },
+ { typeof(DateTimeOffset), "TEXT" },
+ { typeof(decimal), "TEXT" },
+ { typeof(double), "REAL" },
+ { typeof(Guid), "TEXT" },
+ { typeof(short), "INTEGER" },
+ { typeof(int), "INTEGER" },
+ { typeof(long), "INTEGER" },
+ { typeof(float), "REAL" },
+ { typeof(string), "TEXT" },
+ { typeof(TimeOnly), "TEXT" },
+ { typeof(TimeSpan), "TEXT" },
+ };
+
+ private readonly SqliteEntityManipulator entityManipulator;
+ private readonly SqliteTemporaryTableBuilder temporaryTableBuilder;
+
///
/// Initializes a new instance of the class.
///
@@ -23,11 +47,10 @@ public SqliteDatabaseAdapter()
public IEntityManipulator EntityManipulator => this.entityManipulator;
///
- public ITemporaryTableBuilder TemporaryTableBuilder =>
- this.temporaryTableBuilder;
+ public ITemporaryTableBuilder TemporaryTableBuilder => this.temporaryTableBuilder;
///
- public void BindParameterValue(DbParameter parameter, Object? value)
+ public void BindParameterValue(DbParameter parameter, object? value)
{
ArgumentNullException.ThrowIfNull(parameter);
@@ -41,16 +64,13 @@ public void BindParameterValue(DbParameter parameter, Object? value)
case Enum enumValue:
parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch
{
- EnumSerializationMode.Integers =>
- DbType.Int32,
+ EnumSerializationMode.Integers => DbType.Int32,
- EnumSerializationMode.Strings =>
- DbType.String,
+ EnumSerializationMode.Strings => DbType.String,
- _ =>
- ThrowHelper.ThrowInvalidEnumSerializationModeException(
- DbConnectionPlusConfiguration.Instance.EnumSerializationMode
- )
+ _ => ThrowHelper.ThrowInvalidEnumSerializationModeException(
+ DbConnectionPlusConfiguration.Instance.EnumSerializationMode
+ ),
};
parameter.Value = EnumSerializer.SerializeEnum(
@@ -59,7 +79,7 @@ public void BindParameterValue(DbParameter parameter, Object? value)
);
break;
- case Byte[]:
+ case byte[]:
parameter.DbType = DbType.Binary;
parameter.Value = value;
break;
@@ -71,11 +91,10 @@ public void BindParameterValue(DbParameter parameter, Object? value)
}
///
- public String FormatParameterName(String parameterName) =>
- "@" + parameterName;
+ public string FormatParameterName(string parameterName) => "@" + parameterName;
///
- public String GetDataType(Type type, EnumSerializationMode enumSerializationMode)
+ public string GetDataType(Type type, EnumSerializationMode enumSerializationMode)
{
ArgumentNullException.ThrowIfNull(type);
@@ -86,14 +105,11 @@ public String GetDataType(Type type, EnumSerializationMode enumSerializationMode
{
return enumSerializationMode switch
{
- EnumSerializationMode.Strings =>
- "TEXT",
+ EnumSerializationMode.Strings => "TEXT",
- EnumSerializationMode.Integers =>
- "INTEGER",
+ EnumSerializationMode.Integers => "INTEGER",
- _ =>
- ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode)
+ _ => ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode),
};
}
@@ -110,47 +126,20 @@ public String GetDataType(Type type, EnumSerializationMode enumSerializationMode
}
///
- public String QuoteIdentifier(String identifier) =>
- "\"" + identifier + "\"";
+ public string QuoteIdentifier(string identifier) => "\"" + identifier + "\"";
///
- public String QuoteTemporaryTableName(String tableName, DbConnection connection) =>
- "temp.\"" + tableName + "\"";
+ public string QuoteTemporaryTableName(string tableName, DbConnection connection) => "temp.\"" + tableName + "\"";
///
- public Boolean SupportsTemporaryTables(DbConnection connection) =>
- true;
+ public bool SupportsTemporaryTables(DbConnection connection) => true;
///
- public Boolean WasSqlStatementCancelledByCancellationToken(Exception exception, CancellationToken cancellationToken)
+ public bool WasSqlStatementCancelledByCancellationToken(Exception exception, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(exception);
// SQLite does not support proper statement cancellation.
return false;
}
-
- private readonly SqliteEntityManipulator entityManipulator;
- private readonly SqliteTemporaryTableBuilder temporaryTableBuilder;
-
- private static readonly Dictionary typeToSqliteDataType = new()
- {
- { typeof(Boolean), "INTEGER" },
- { typeof(Byte), "INTEGER" },
- { typeof(Byte[]), "BLOB" },
- { typeof(Char), "TEXT" },
- { typeof(DateOnly), "TEXT" },
- { typeof(DateTime), "TEXT" },
- { typeof(DateTimeOffset), "TEXT" },
- { typeof(Decimal), "TEXT" },
- { typeof(Double), "REAL" },
- { typeof(Guid), "TEXT" },
- { typeof(Int16), "INTEGER" },
- { typeof(Int32), "INTEGER" },
- { typeof(Int64), "INTEGER" },
- { typeof(Single), "REAL" },
- { typeof(String), "TEXT" },
- { typeof(TimeOnly), "TEXT" },
- { typeof(TimeSpan), "TEXT" }
- };
}
diff --git a/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteEntityManipulator.cs b/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteEntityManipulator.cs
index 080c8b9..811c7b3 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteEntityManipulator.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteEntityManipulator.cs
@@ -4,26 +4,22 @@
using LinkDotNet.StringBuilder;
using RentADeveloper.DbConnectionPlus.Converters;
using RentADeveloper.DbConnectionPlus.DbCommands;
-using RentADeveloper.DbConnectionPlus.Entities;
namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.Sqlite;
///
/// The entity manipulator for SQLite.
///
-internal class SqliteEntityManipulator : IEntityManipulator
+/// The database adapter to use to manipulate entities.
+internal class SqliteEntityManipulator(SqliteDatabaseAdapter databaseAdapter) : IEntityManipulator
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The database adapter to use to manipulate entities.
- public SqliteEntityManipulator(SqliteDatabaseAdapter databaseAdapter) =>
- this.databaseAdapter = databaseAdapter;
+ private readonly SqliteDatabaseAdapter databaseAdapter = databaseAdapter;
+ private readonly ConcurrentDictionary entityDeleteSqlCodePerEntityType = new();
+ private readonly ConcurrentDictionary entityInsertSqlCodePerEntityType = new();
+ private readonly ConcurrentDictionary entityUpdateSqlCodePerEntityType = new();
///
- public Int32 DeleteEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int DeleteEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -70,11 +66,8 @@ CancellationToken cancellationToken
totalNumberOfAffectedRows += numberOfAffectedRows;
}
}
- catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -84,9 +77,7 @@ CancellationToken cancellationToken
}
///
- public async Task DeleteEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task DeleteEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -119,8 +110,9 @@ CancellationToken cancellationToken
DbConnectionExtensions.OnBeforeExecutingCommand(command, []);
- var numberOfAffectedRows =
- await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
+ var numberOfAffectedRows = await command
+ .ExecuteNonQueryAsync(cancellationToken)
+ .ConfigureAwait(false);
if (numberOfAffectedRows != 1)
{
@@ -134,11 +126,8 @@ CancellationToken cancellationToken
totalNumberOfAffectedRows += numberOfAffectedRows;
}
}
- catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -148,9 +137,7 @@ CancellationToken cancellationToken
}
///
- public Int32 DeleteEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int DeleteEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -187,11 +174,8 @@ CancellationToken cancellationToken
return numberOfAffectedRows;
}
- catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -199,9 +183,7 @@ CancellationToken cancellationToken
}
///
- public async Task DeleteEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task DeleteEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -238,11 +220,8 @@ CancellationToken cancellationToken
return numberOfAffectedRows;
}
- catch (Exception exception) when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -250,9 +229,7 @@ CancellationToken cancellationToken
}
///
- public Int32 InsertEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int InsertEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -264,11 +241,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateInsertEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateInsertEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -296,9 +269,8 @@ CancellationToken cancellationToken
totalNumberOfAffectedRows += reader.RecordsAffected;
}
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -308,9 +280,7 @@ CancellationToken cancellationToken
}
///
- public async Task InsertEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task InsertEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -322,11 +292,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateInsertEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateInsertEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -349,22 +315,18 @@ CancellationToken cancellationToken
#pragma warning disable CA2007
await using var reader = await command
- .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken).ConfigureAwait(false);
+ .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)
+ .ConfigureAwait(false);
#pragma warning restore CA2007
- await UpdateDatabaseGeneratedPropertiesAsync(
- entityTypeMetadata,
- reader,
- entity,
- cancellationToken
- ).ConfigureAwait(false);
+ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken)
+ .ConfigureAwait(false);
totalNumberOfAffectedRows += reader.RecordsAffected;
}
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -374,9 +336,7 @@ await UpdateDatabaseGeneratedPropertiesAsync(
}
///
- public Int32 InsertEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int InsertEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -388,11 +348,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateInsertEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateInsertEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -410,9 +366,8 @@ CancellationToken cancellationToken
return reader.RecordsAffected;
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -420,9 +375,7 @@ CancellationToken cancellationToken
}
///
- public async Task InsertEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task InsertEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -434,11 +387,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateInsertEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateInsertEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -452,7 +401,8 @@ CancellationToken cancellationToken
#pragma warning disable CA2007
await using var reader = await command
- .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken).ConfigureAwait(false);
+ .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)
+ .ConfigureAwait(false);
#pragma warning restore CA2007
await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken)
@@ -460,9 +410,8 @@ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity,
return reader.RecordsAffected;
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -470,9 +419,7 @@ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity,
}
///
- public Int32 UpdateEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int UpdateEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -484,11 +431,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateUpdateEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateUpdateEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -530,9 +473,8 @@ CancellationToken cancellationToken
totalNumberOfAffectedRows += reader.RecordsAffected;
}
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -542,9 +484,7 @@ CancellationToken cancellationToken
}
///
- public async Task UpdateEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task UpdateEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -556,11 +496,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateUpdateEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateUpdateEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -583,15 +519,12 @@ CancellationToken cancellationToken
#pragma warning disable CA2007
await using var reader = await command
- .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken).ConfigureAwait(false);
+ .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)
+ .ConfigureAwait(false);
#pragma warning restore CA2007
- await UpdateDatabaseGeneratedPropertiesAsync(
- entityTypeMetadata,
- reader,
- entity,
- cancellationToken
- ).ConfigureAwait(false);
+ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity, cancellationToken)
+ .ConfigureAwait(false);
// We must close the reader before we can access DbDataReader.RecordsAffected, because otherwise it
// returns -1 when we select database generated properties via the SELECT statement after the
@@ -610,9 +543,8 @@ await UpdateDatabaseGeneratedPropertiesAsync(
totalNumberOfAffectedRows += reader.RecordsAffected;
}
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -622,9 +554,7 @@ await UpdateDatabaseGeneratedPropertiesAsync(
}
///
- public Int32 UpdateEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int UpdateEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -636,11 +566,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateUpdateEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateUpdateEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -672,9 +598,8 @@ CancellationToken cancellationToken
return reader.RecordsAffected;
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -682,9 +607,7 @@ CancellationToken cancellationToken
}
///
- public async Task UpdateEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public async Task UpdateEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -696,11 +619,7 @@ CancellationToken cancellationToken
var entityTypeMetadata = EntityHelper.GetEntityTypeMetadata(typeof(TEntity));
- var (command, parameters) = this.CreateUpdateEntityCommand(
- connection,
- transaction,
- entityTypeMetadata
- );
+ var (command, parameters) = this.CreateUpdateEntityCommand(connection, transaction, entityTypeMetadata);
var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken);
using (command)
@@ -737,15 +656,89 @@ await UpdateDatabaseGeneratedPropertiesAsync(entityTypeMetadata, reader, entity,
return reader.RecordsAffected;
}
- catch (Exception exception) when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
}
}
+ ///
+ /// Updates the database generated properties of the provided entity from the provided data reader.
+ ///
+ /// The metadata for the entity type.
+ /// The data reader from which to read the values for the properties.
+ /// The entity to update.
+ /// A token that can be used to cancel the operation.
+ private static void UpdateDatabaseGeneratedProperties(
+ EntityTypeMetadata entityTypeMetadata,
+ DbDataReader reader,
+ object entity,
+ CancellationToken cancellationToken
+ )
+ {
+ if (entityTypeMetadata.DatabaseGeneratedProperties.Count > 0 && reader.Read())
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++)
+ {
+ var property = entityTypeMetadata.DatabaseGeneratedProperties[i];
+
+ if (!property.CanWrite)
+ {
+ continue;
+ }
+
+ var value = reader.GetValue(i);
+
+ value = ValueConverter.ConvertValueToType(value, property.PropertyType);
+
+ property.PropertySetter!(entity, value);
+ }
+ }
+ }
+
+ ///
+ /// Asynchronously updates the database generated properties of the provided entity from the provided data
+ /// reader.
+ ///
+ /// The metadata for the entity type.
+ /// The data reader from which to read the values for the properties.
+ /// The entity to update.
+ /// A token that can be used to cancel the operation.
+ /// A task that represents the asynchronous operation.
+ private static async Task UpdateDatabaseGeneratedPropertiesAsync(
+ EntityTypeMetadata entityTypeMetadata,
+ DbDataReader reader,
+ object entity,
+ CancellationToken cancellationToken
+ )
+ {
+ if (
+ entityTypeMetadata.DatabaseGeneratedProperties.Count > 0
+ && await reader.ReadAsync(cancellationToken).ConfigureAwait(false)
+ )
+ {
+ for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++)
+ {
+ var property = entityTypeMetadata.DatabaseGeneratedProperties[i];
+
+ if (!property.CanWrite)
+ {
+ continue;
+ }
+
+ var value = reader.GetValue(i);
+
+ value = ValueConverter.ConvertValueToType(value, property.PropertyType);
+
+ property.PropertySetter!(entity, value);
+ }
+ }
+ }
+
///
/// Creates a command to delete an entity.
///
@@ -771,8 +764,8 @@ EntityTypeMetadata entityTypeMetadata
var parameters = new List();
- var whereProperties = entityTypeMetadata.KeyProperties
- .Concat(entityTypeMetadata.ConcurrencyTokenProperties)
+ var whereProperties = entityTypeMetadata
+ .KeyProperties.Concat(entityTypeMetadata.ConcurrencyTokenProperties)
.Concat(entityTypeMetadata.RowVersionProperties);
foreach (var property in whereProperties)
@@ -863,7 +856,7 @@ EntityTypeMetadata entityTypeMetadata
///
/// The metadata for the entity type to delete.
/// The SQL code to delete an entity of the specified type.
- private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
+ private string GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
this.entityDeleteSqlCodePerEntityType.GetOrAdd(
entityTypeMetadata.EntityType,
_ =>
@@ -873,7 +866,7 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
ThrowHelper.ThrowEntityTypeHasNoKeyPropertyException(entityTypeMetadata.EntityType);
}
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]);
sqlBuilder.AppendLine("DELETE FROM");
@@ -888,8 +881,8 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
var prependSeparator = false;
- var whereProperties = entityTypeMetadata.KeyProperties
- .Concat(entityTypeMetadata.ConcurrencyTokenProperties)
+ var whereProperties = entityTypeMetadata
+ .KeyProperties.Concat(entityTypeMetadata.ConcurrencyTokenProperties)
.Concat(entityTypeMetadata.RowVersionProperties);
foreach (var keyProperty in whereProperties)
@@ -918,12 +911,12 @@ private String GetDeleteEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
///
/// The metadata for the entity type to insert.
/// The SQL code to insert an entity of the specified type.
- private String GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
+ private string GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
this.entityInsertSqlCodePerEntityType.GetOrAdd(
entityTypeMetadata.EntityType,
_ =>
{
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]);
sqlBuilder.Append("INSERT INTO \"");
sqlBuilder.Append(entityTypeMetadata.TableName);
@@ -1049,7 +1042,7 @@ private String GetInsertEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
///
/// The metadata for the entity type to update.
/// The SQL code to update an entity of the specified type.
- private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
+ private string GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
this.entityUpdateSqlCodePerEntityType.GetOrAdd(
entityTypeMetadata.EntityType,
_ =>
@@ -1059,7 +1052,7 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
ThrowHelper.ThrowEntityTypeHasNoKeyPropertyException(entityTypeMetadata.EntityType);
}
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]);
sqlBuilder.AppendLine("UPDATE");
@@ -1098,8 +1091,8 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
prependSeparator = false;
- var whereProperties = entityTypeMetadata.KeyProperties
- .Concat(entityTypeMetadata.ConcurrencyTokenProperties)
+ var whereProperties = entityTypeMetadata
+ .KeyProperties.Concat(entityTypeMetadata.ConcurrencyTokenProperties)
.Concat(entityTypeMetadata.RowVersionProperties)
.ToList();
@@ -1163,7 +1156,7 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) =>
whereProperties =
[
.. entityTypeMetadata.KeyProperties,
- .. entityTypeMetadata.ConcurrencyTokenProperties
+ .. entityTypeMetadata.ConcurrencyTokenProperties,
];
foreach (var keyProperty in whereProperties)
@@ -1198,7 +1191,7 @@ .. entityTypeMetadata.ConcurrencyTokenProperties
private void PopulateParametersFromEntityProperties(
EntityTypeMetadata entityTypeMetadata,
List parameters,
- Object entity
+ object entity
)
{
ArgumentNullException.ThrowIfNull(parameters);
@@ -1211,84 +1204,4 @@ Object entity
this.databaseAdapter.BindParameterValue(parameter, propertyValue);
}
}
-
- ///
- /// Updates the database generated properties of the provided entity from the provided data reader.
- ///
- /// The metadata for the entity type.
- /// The data reader from which to read the values for the properties.
- /// The entity to update.
- /// A token that can be used to cancel the operation.
- private static void UpdateDatabaseGeneratedProperties(
- EntityTypeMetadata entityTypeMetadata,
- DbDataReader reader,
- Object entity,
- CancellationToken cancellationToken
- )
- {
- if (entityTypeMetadata.DatabaseGeneratedProperties.Count > 0 && reader.Read())
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++)
- {
- var property = entityTypeMetadata.DatabaseGeneratedProperties[i];
-
- if (!property.CanWrite)
- {
- continue;
- }
-
- var value = reader.GetValue(i);
-
- value = ValueConverter.ConvertValueToType(value, property.PropertyType);
-
- property.PropertySetter!(entity, value);
- }
- }
- }
-
- ///
- /// Asynchronously updates the database generated properties of the provided entity from the provided data
- /// reader.
- ///
- /// The metadata for the entity type.
- /// The data reader from which to read the values for the properties.
- /// The entity to update.
- /// A token that can be used to cancel the operation.
- /// A task that represents the asynchronous operation.
- private static async Task UpdateDatabaseGeneratedPropertiesAsync(
- EntityTypeMetadata entityTypeMetadata,
- DbDataReader reader,
- Object entity,
- CancellationToken cancellationToken
- )
- {
- if (
- entityTypeMetadata.DatabaseGeneratedProperties.Count > 0 &&
- await reader.ReadAsync(cancellationToken).ConfigureAwait(false)
- )
- {
- for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++)
- {
- var property = entityTypeMetadata.DatabaseGeneratedProperties[i];
-
- if (!property.CanWrite)
- {
- continue;
- }
-
- var value = reader.GetValue(i);
-
- value = ValueConverter.ConvertValueToType(value, property.PropertyType);
-
- property.PropertySetter!(entity, value);
- }
- }
- }
-
- private readonly SqliteDatabaseAdapter databaseAdapter;
- private readonly ConcurrentDictionary entityDeleteSqlCodePerEntityType = new();
- private readonly ConcurrentDictionary entityInsertSqlCodePerEntityType = new();
- private readonly ConcurrentDictionary entityUpdateSqlCodePerEntityType = new();
}
diff --git a/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteTemporaryTableBuilder.cs b/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteTemporaryTableBuilder.cs
index 45d7577..ea9e86e 100644
--- a/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteTemporaryTableBuilder.cs
+++ b/src/DbConnectionPlus.DatabaseAdapters.Sqlite/SqliteTemporaryTableBuilder.cs
@@ -5,7 +5,6 @@
using Microsoft.Data.Sqlite;
using RentADeveloper.DbConnectionPlus.Converters;
using RentADeveloper.DbConnectionPlus.DbCommands;
-using RentADeveloper.DbConnectionPlus.Entities;
using RentADeveloper.DbConnectionPlus.Extensions;
using RentADeveloper.DbConnectionPlus.Readers;
@@ -16,6 +15,8 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.Sqlite;
///
internal class SqliteTemporaryTableBuilder : ITemporaryTableBuilder
{
+ private readonly SqliteDatabaseAdapter databaseAdapter;
+
///
/// Initializes a new instance of the class.
///
@@ -34,10 +35,9 @@ public SqliteTemporaryTableBuilder(SqliteDatabaseAdapter databaseAdapter)
public TemporaryTableDisposer BuildTemporaryTable(
DbConnection connection,
DbTransaction? transaction,
- String name,
+ string name,
IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
CancellationToken cancellationToken = default
)
{
@@ -69,8 +69,10 @@ public TemporaryTableDisposer BuildTemporaryTable(
);
createCommand.Transaction = transaction;
- using var cancellationTokenRegistration =
- DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken);
+ using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(
+ createCommand,
+ cancellationToken
+ );
DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []);
@@ -87,8 +89,10 @@ public TemporaryTableDisposer BuildTemporaryTable(
);
createCommand.Transaction = transaction;
- using var cancellationTokenRegistration =
- DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken);
+ using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation(
+ createCommand,
+ cancellationToken
+ );
DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []);
@@ -109,10 +113,9 @@ public TemporaryTableDisposer BuildTemporaryTable(
public async Task BuildTemporaryTableAsync(
DbConnection connection,
DbTransaction? transaction,
- String name,
+ string name,
IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
CancellationToken cancellationToken = default
)
{
@@ -146,8 +149,9 @@ public async Task BuildTemporaryTableAsync(
);
createCommand.Transaction = transaction;
- await using var cancellationTokenRegistration =
- DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken).ConfigureAwait(false);
+ await using var cancellationTokenRegistration = DbCommandHelper
+ .RegisterDbCommandCancellation(createCommand, cancellationToken)
+ .ConfigureAwait(false);
DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []);
@@ -166,8 +170,9 @@ public async Task BuildTemporaryTableAsync(
);
createCommand.Transaction = transaction;
- await using var cancellationTokenRegistration =
- DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken).ConfigureAwait(false);
+ await using var cancellationTokenRegistration = DbCommandHelper
+ .RegisterDbCommandCancellation(createCommand, cancellationToken)
+ .ConfigureAwait(false);
DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []);
@@ -194,88 +199,6 @@ await PopulateTemporaryTableAsync(
);
}
- ///
- /// Builds an SQL code to create a multi-column temporary table to be populated with objects of the type
- /// .
- ///
- /// The name of the table to create.
- /// The type of objects with which to populate the table.
- /// The mode to use to serialize values.
- /// The built SQL code.
- private String BuildCreateMultiColumnTemporaryTableSqlCode(
- String tableName,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type objectsType,
- EnumSerializationMode enumSerializationMode
- )
- {
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
-
- sqlBuilder.Append("CREATE TEMP TABLE \"");
- sqlBuilder.Append(tableName);
- sqlBuilder.AppendLine("\"");
-
- sqlBuilder.Append(Constants.Indent);
- sqlBuilder.Append("(");
-
- var properties = EntityHelper.GetEntityTypeMetadata(objectsType).MappedProperties.Where(a => a.CanRead);
-
- var prependSeparator = false;
-
- foreach (var property in properties)
- {
- if (prependSeparator)
- {
- sqlBuilder.Append(", ");
- }
-
- sqlBuilder.Append('"');
- sqlBuilder.Append(property.ColumnName);
- sqlBuilder.Append("\" ");
-
- var propertyType = property.PropertyType;
-
- sqlBuilder.Append(this.databaseAdapter.GetDataType(propertyType, enumSerializationMode));
-
- prependSeparator = true;
- }
-
- sqlBuilder.AppendLine(")");
-
- return sqlBuilder.ToString();
- }
-
- ///
- /// Builds an SQL code to create a single-column temporary table to be populated with values of the type
- /// .
- ///
- /// The name of the table to create.
- /// The type of values with which the table will be populated.
- /// The mode to use to serialize values.
- /// The built SQL code.
- private String BuildCreateSingleColumnTemporaryTableSqlCode(
- String tableName,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
- EnumSerializationMode enumSerializationMode
- )
- {
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[100]);
-
- sqlBuilder.Append("CREATE TEMP TABLE \"");
- sqlBuilder.Append(tableName);
- sqlBuilder.AppendLine("\"");
-
- sqlBuilder.Append(Constants.Indent);
- sqlBuilder.Append("(\"");
- sqlBuilder.Append(Constants.SingleColumnTemporaryTableColumnName);
- sqlBuilder.Append("\" ");
- sqlBuilder.Append(this.databaseAdapter.GetDataType(valuesType, enumSerializationMode));
- sqlBuilder.AppendLine(")");
-
- return sqlBuilder.ToString();
- }
-
///
/// Builds an SQL code to insert data from the specified data reader into the specified temporary table.
///
@@ -283,14 +206,13 @@ EnumSerializationMode enumSerializationMode
/// The type of values with which to populate the table.
/// The data reader to read data from.
/// A tuple containing the insert SQL code and the parameters to use.
- private static (String SqlCode, SqliteParameter[] Parameters) BuildInsertSqlCode(
- String tableName,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ private static (string SqlCode, SqliteParameter[] Parameters) BuildInsertSqlCode(
+ string tableName,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
DbDataReader dataReader
)
{
- using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]);
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]);
sqlBuilder.Append("INSERT INTO temp.\"");
sqlBuilder.Append(tableName);
@@ -306,14 +228,13 @@ DbDataReader dataReader
{
sqlBuilder.Append(Constants.SingleColumnTemporaryTableColumnName);
- parameters[0] = new()
- {
- ParameterName = Constants.SingleColumnTemporaryTableColumnName
- };
+ parameters[0] = new() { ParameterName = Constants.SingleColumnTemporaryTableColumnName };
}
else
{
- var properties = EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead)
+ var properties = EntityHelper
+ .GetEntityTypeMetadata(valuesType)
+ .MappedProperties.Where(a => a.CanRead)
.ToList();
for (var i = 0; i < properties.Count; i++)
@@ -329,10 +250,7 @@ DbDataReader dataReader
sqlBuilder.Append(property.ColumnName);
sqlBuilder.Append('"');
- parameters[i] = new()
- {
- ParameterName = property.PropertyName
- };
+ parameters[i] = new() { ParameterName = property.PropertyName };
}
}
@@ -367,15 +285,15 @@ DbDataReader dataReader
/// A that provides access to the data in .
private static EnumerableReader CreateValuesDataReader(
IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType)
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType
+ )
{
if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType())
{
- return new EnumerableReader(values, valuesType, Constants.SingleColumnTemporaryTableColumnName);
+ return new(values, valuesType, Constants.SingleColumnTemporaryTableColumnName);
}
- return new EnumerableReader(
+ return new(
values,
[.. EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead)],
EnumerableReaderOptions.None
@@ -388,7 +306,7 @@ [.. EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a
/// The name of the table to drop.
/// The connection to use to drop the table.
/// The transaction within to drop the table.
- private static void DropTemporaryTable(String name, SqliteConnection connection, SqliteTransaction? transaction)
+ private static void DropTemporaryTable(string name, SqliteConnection connection, SqliteTransaction? transaction)
{
using var command = connection.CreateCommand();
@@ -408,7 +326,7 @@ private static void DropTemporaryTable(String name, SqliteConnection connection,
/// The transaction within to drop the table.
/// A task representing the asynchronous operation.
private static async ValueTask DropTemporaryTableAsync(
- String name,
+ string name,
SqliteConnection connection,
SqliteTransaction? transaction
)
@@ -437,9 +355,8 @@ private static async ValueTask DropTemporaryTableAsync(
private static void PopulateTemporaryTable(
SqliteConnection connection,
SqliteTransaction? transaction,
- String tableName,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ string tableName,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
DbDataReader dataReader,
CancellationToken cancellationToken
)
@@ -493,9 +410,8 @@ CancellationToken cancellationToken
private static async Task PopulateTemporaryTableAsync(
SqliteConnection connection,
SqliteTransaction? transaction,
- String tableName,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ string tableName,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
DbDataReader dataReader,
CancellationToken cancellationToken
)
@@ -537,5 +453,83 @@ CancellationToken cancellationToken
}
}
- private readonly SqliteDatabaseAdapter databaseAdapter;
+ ///
+ /// Builds an SQL code to create a multi-column temporary table to be populated with objects of the type
+ /// .
+ ///
+ /// The name of the table to create.
+ /// The type of objects with which to populate the table.
+ /// The mode to use to serialize values.
+ /// The built SQL code.
+ private string BuildCreateMultiColumnTemporaryTableSqlCode(
+ string tableName,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type objectsType,
+ EnumSerializationMode enumSerializationMode
+ )
+ {
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]);
+
+ sqlBuilder.Append("CREATE TEMP TABLE \"");
+ sqlBuilder.Append(tableName);
+ sqlBuilder.AppendLine("\"");
+
+ sqlBuilder.Append(Constants.Indent);
+ sqlBuilder.Append("(");
+
+ var properties = EntityHelper.GetEntityTypeMetadata(objectsType).MappedProperties.Where(a => a.CanRead);
+
+ var prependSeparator = false;
+
+ foreach (var property in properties)
+ {
+ if (prependSeparator)
+ {
+ sqlBuilder.Append(", ");
+ }
+
+ sqlBuilder.Append('"');
+ sqlBuilder.Append(property.ColumnName);
+ sqlBuilder.Append("\" ");
+
+ var propertyType = property.PropertyType;
+
+ sqlBuilder.Append(this.databaseAdapter.GetDataType(propertyType, enumSerializationMode));
+
+ prependSeparator = true;
+ }
+
+ sqlBuilder.AppendLine(")");
+
+ return sqlBuilder.ToString();
+ }
+
+ ///
+ /// Builds an SQL code to create a single-column temporary table to be populated with values of the type
+ /// .
+ ///
+ /// The name of the table to create.
+ /// The type of values with which the table will be populated.
+ /// The mode to use to serialize values.
+ /// The built SQL code.
+ private string BuildCreateSingleColumnTemporaryTableSqlCode(
+ string tableName,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
+ EnumSerializationMode enumSerializationMode
+ )
+ {
+ using var sqlBuilder = new ValueStringBuilder(stackalloc char[100]);
+
+ sqlBuilder.Append("CREATE TEMP TABLE \"");
+ sqlBuilder.Append(tableName);
+ sqlBuilder.AppendLine("\"");
+
+ sqlBuilder.Append(Constants.Indent);
+ sqlBuilder.Append("(\"");
+ sqlBuilder.Append(Constants.SingleColumnTemporaryTableColumnName);
+ sqlBuilder.Append("\" ");
+ sqlBuilder.Append(this.databaseAdapter.GetDataType(valuesType, enumSerializationMode));
+ sqlBuilder.AppendLine(")");
+
+ return sqlBuilder.ToString();
+ }
}
diff --git a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs
index bd68fdd..418f914 100644
--- a/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs
+++ b/src/DbConnectionPlus/Configuration/DbConnectionPlusConfiguration.cs
@@ -5,12 +5,19 @@ namespace RentADeveloper.DbConnectionPlus.Configuration;
///
public sealed class DbConnectionPlusConfiguration : IFreezable
{
+ private readonly Dictionary databaseAdapters = [];
+ private readonly Dictionary entityTypeBuilders = [];
+ private bool isFrozen;
+
///
/// Initializes a new instance of the class.
///
- internal DbConnectionPlusConfiguration()
- {
- }
+ internal DbConnectionPlusConfiguration() { }
+
+ ///
+ /// The singleton instance of .
+ ///
+ public static DbConnectionPlusConfiguration Instance { get; internal set; } = new();
///
///
@@ -77,6 +84,17 @@ public InterceptDbCommand? InterceptDbCommand
}
}
+ ///
+ void IFreezable.Freeze()
+ {
+ this.isFrozen = true;
+
+ foreach (var entityTypeBuilder in this.entityTypeBuilders.Values)
+ {
+ entityTypeBuilder.Freeze();
+ }
+ }
+
///
/// Gets a builder for configuring the entity type .
///
@@ -119,22 +137,6 @@ public void RegisterDatabaseAdapter(IDatabaseAdapter adapter)
this.databaseAdapters[typeof(TConnection)] = adapter;
}
- ///
- void IFreezable.Freeze()
- {
- this.isFrozen = true;
-
- foreach (var entityTypeBuilder in this.entityTypeBuilders.Values)
- {
- entityTypeBuilder.Freeze();
- }
- }
-
- ///
- /// The singleton instance of .
- ///
- public static DbConnectionPlusConfiguration Instance { get; internal set; } = new();
-
///
/// Retrieves the database adapter associated with the connection type .
///
@@ -158,11 +160,11 @@ internal IDatabaseAdapter GetDatabaseAdapter(Type connectionType)
return this.databaseAdapters.TryGetValue(connectionType, out var adapter)
? adapter
: throw new InvalidOperationException(
- $"No database adapter is registered for the database connection of the type {connectionType}. " +
- "Please install the corresponding adapter NuGet package " +
- "(e.g., RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer) " +
- "and register it by calling the appropriate UseXxx() extension method via " +
- $"{nameof(DbConnectionExtensions)}.{nameof(DbConnectionExtensions.Configure)}."
+ $"No database adapter is registered for the database connection of the type {connectionType}. "
+ + "Please install the corresponding adapter NuGet package "
+ + "(e.g., RentADeveloper.DbConnectionPlus.DatabaseAdapters.SqlServer) "
+ + "and register it by calling the appropriate UseXxx() extension method via "
+ + $"{nameof(DbConnectionExtensions)}.{nameof(DbConnectionExtensions.Configure)}."
);
}
@@ -183,8 +185,4 @@ private void EnsureNotFrozen()
ThrowHelper.ThrowConfigurationIsFrozenException();
}
}
-
- private readonly Dictionary databaseAdapters = [];
- private readonly Dictionary entityTypeBuilders = [];
- private Boolean isFrozen;
}
diff --git a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs
index 3a8e929..95d05b4 100644
--- a/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs
+++ b/src/DbConnectionPlus/Configuration/EntityPropertyBuilder.cs
@@ -5,6 +5,18 @@ namespace RentADeveloper.DbConnectionPlus.Configuration;
///
public sealed class EntityPropertyBuilder : IEntityPropertyBuilder
{
+ private readonly IEntityTypeBuilder entityTypeBuilder;
+ private readonly string propertyName;
+
+ private string? columnName;
+ private bool isComputed;
+ private bool isConcurrencyToken;
+ private bool isFrozen;
+ private bool isIdentity;
+ private bool isIgnored;
+ private bool isKey;
+ private bool isRowVersion;
+
///
/// Initializes a new instance of the class.
///
@@ -25,7 +37,7 @@ public sealed class EntityPropertyBuilder : IEntityPropertyBuilder
///
///
/// is whitespace.
- internal EntityPropertyBuilder(IEntityTypeBuilder entityTypeBuilder, String propertyName)
+ internal EntityPropertyBuilder(IEntityTypeBuilder entityTypeBuilder, string propertyName)
{
ArgumentNullException.ThrowIfNull(entityTypeBuilder);
ArgumentException.ThrowIfNullOrWhiteSpace(propertyName);
@@ -34,6 +46,33 @@ internal EntityPropertyBuilder(IEntityTypeBuilder entityTypeBuilder, String prop
this.propertyName = propertyName;
}
+ ///
+ string? IEntityPropertyBuilder.ColumnName => this.columnName;
+
+ ///
+ bool IEntityPropertyBuilder.IsComputed => this.isComputed;
+
+ ///
+ bool IEntityPropertyBuilder.IsConcurrencyToken => this.isConcurrencyToken;
+
+ ///
+ bool IEntityPropertyBuilder.IsIdentity => this.isIdentity;
+
+ ///
+ bool IEntityPropertyBuilder.IsIgnored => this.isIgnored;
+
+ ///
+ bool IEntityPropertyBuilder.IsKey => this.isKey;
+
+ ///
+ bool IEntityPropertyBuilder.IsRowVersion => this.isRowVersion;
+
+ ///
+ string IEntityPropertyBuilder.PropertyName => this.propertyName;
+
+ ///
+ void IFreezable.Freeze() => this.isFrozen = true;
+
///
/// Sets the name of the column to map the property to.
///
@@ -43,7 +82,7 @@ internal EntityPropertyBuilder(IEntityTypeBuilder entityTypeBuilder, String prop
/// The configuration of DbConnectionPlus is already frozen and can no longer be modified.
///
// ReSharper disable once ParameterHidesMember
- public EntityPropertyBuilder HasColumnName(String columnName)
+ public EntityPropertyBuilder HasColumnName(string columnName)
{
this.EnsureNotFrozen();
@@ -101,17 +140,16 @@ public EntityPropertyBuilder IsIdentity()
{
this.EnsureNotFrozen();
- var otherIdentityProperty =
- this.entityTypeBuilder.PropertyBuilders.Values.FirstOrDefault(a =>
- a.PropertyName != this.propertyName && a.IsIdentity
- );
+ var otherIdentityProperty = this.entityTypeBuilder.PropertyBuilders.Values.FirstOrDefault(a =>
+ a.PropertyName != this.propertyName && a.IsIdentity
+ );
if (otherIdentityProperty is not null)
{
throw new InvalidOperationException(
- $"There is already the property '{otherIdentityProperty.PropertyName}' marked as an identity " +
- $"property for the entity type {this.entityTypeBuilder.EntityType}. Only one property can be marked " +
- "as identity property per entity type."
+ $"There is already the property '{otherIdentityProperty.PropertyName}' marked as an identity "
+ + $"property for the entity type {this.entityTypeBuilder.EntityType}. Only one property can be marked "
+ + "as identity property per entity type."
);
}
@@ -167,33 +205,6 @@ public EntityPropertyBuilder IsRowVersion()
return this;
}
- ///
- String? IEntityPropertyBuilder.ColumnName => this.columnName;
-
- ///
- void IFreezable.Freeze() => this.isFrozen = true;
-
- ///
- Boolean IEntityPropertyBuilder.IsComputed => this.isComputed;
-
- ///
- Boolean IEntityPropertyBuilder.IsConcurrencyToken => this.isConcurrencyToken;
-
- ///
- Boolean IEntityPropertyBuilder.IsIdentity => this.isIdentity;
-
- ///
- Boolean IEntityPropertyBuilder.IsIgnored => this.isIgnored;
-
- ///
- Boolean IEntityPropertyBuilder.IsKey => this.isKey;
-
- ///
- Boolean IEntityPropertyBuilder.IsRowVersion => this.isRowVersion;
-
- ///
- String IEntityPropertyBuilder.PropertyName => this.propertyName;
-
///
/// Ensures this instance is not frozen.
///
@@ -205,16 +216,4 @@ private void EnsureNotFrozen()
ThrowHelper.ThrowConfigurationIsFrozenException();
}
}
-
- private readonly IEntityTypeBuilder entityTypeBuilder;
- private readonly String propertyName;
-
- private String? columnName;
- private Boolean isComputed;
- private Boolean isConcurrencyToken;
- private Boolean isFrozen;
- private Boolean isIdentity;
- private Boolean isIgnored;
- private Boolean isKey;
- private Boolean isRowVersion;
}
diff --git a/src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs b/src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs
index b4f0f97..dd45f46 100644
--- a/src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs
+++ b/src/DbConnectionPlus/Configuration/EntityTypeBuilder.cs
@@ -9,6 +9,30 @@ namespace RentADeveloper.DbConnectionPlus.Configuration;
/// The type of the entity being configured.
public sealed class EntityTypeBuilder : IEntityTypeBuilder
{
+ private readonly ConcurrentDictionary propertyBuilders = new();
+ private bool isFrozen;
+ private string? tableName;
+
+ ///
+ Type IEntityTypeBuilder.EntityType => typeof(TEntity);
+
+ ///
+ IReadOnlyDictionary IEntityTypeBuilder.PropertyBuilders => this.propertyBuilders;
+
+ ///
+ string? IEntityTypeBuilder.TableName => this.tableName;
+
+ ///
+ void IFreezable.Freeze()
+ {
+ this.isFrozen = true;
+
+ foreach (var propertyBuilder in this.propertyBuilders.Values)
+ {
+ propertyBuilder.Freeze();
+ }
+ }
+
///
/// Gets a builder for configuring the specified property.
///
@@ -34,11 +58,12 @@ public EntityPropertyBuilder Property(Expression new EntityPropertyBuilder(self, propertyName2),
- this
- );
+ return (EntityPropertyBuilder)
+ this.propertyBuilders.GetOrAdd(
+ propertyName,
+ static (propertyName2, self) => new EntityPropertyBuilder(self, propertyName2),
+ this
+ );
}
///
@@ -50,7 +75,7 @@ public EntityPropertyBuilder Property(Expression
// ReSharper disable once ParameterHidesMember
- public EntityTypeBuilder ToTable(String tableName)
+ public EntityTypeBuilder ToTable(string tableName)
{
this.EnsureNotFrozen();
@@ -59,39 +84,6 @@ public EntityTypeBuilder ToTable(String tableName)
return this;
}
- ///
- Type IEntityTypeBuilder.EntityType => typeof(TEntity);
-
- ///
- void IFreezable.Freeze()
- {
- this.isFrozen = true;
-
- foreach (var propertyBuilder in this.propertyBuilders.Values)
- {
- propertyBuilder.Freeze();
- }
- }
-
- ///
- IReadOnlyDictionary IEntityTypeBuilder.PropertyBuilders =>
- this.propertyBuilders;
-
- ///
- String? IEntityTypeBuilder.TableName => this.tableName;
-
- ///
- /// Ensures this instance is not frozen.
- ///
- /// This object is already frozen.
- private void EnsureNotFrozen()
- {
- if (this.isFrozen)
- {
- ThrowHelper.ThrowConfigurationIsFrozenException();
- }
- }
-
///
/// Gets the name of the property accessed in the specified property access expression.
///
@@ -100,16 +92,24 @@ private void EnsureNotFrozen()
///
/// is not a valid property access expression.
///
- private static String GetPropertyNameFromPropertyExpression(LambdaExpression propertyExpression) =>
+ private static string GetPropertyNameFromPropertyExpression(LambdaExpression propertyExpression) =>
propertyExpression.Body is MemberExpression { Member: PropertyInfo propertyInfo }
? propertyInfo.Name
: throw new ArgumentException(
- $"The expression '{propertyExpression}' is not a valid property access expression. The expression should " +
- "represent a simple property access: 'a => a.MyProperty'.",
+ $"The expression '{propertyExpression}' is not a valid property access expression. The expression should "
+ + "represent a simple property access: 'a => a.MyProperty'.",
nameof(propertyExpression)
);
- private readonly ConcurrentDictionary propertyBuilders = new();
- private Boolean isFrozen;
- private String? tableName;
+ ///
+ /// Ensures this instance is not frozen.
+ ///
+ /// This object is already frozen.
+ private void EnsureNotFrozen()
+ {
+ if (this.isFrozen)
+ {
+ ThrowHelper.ThrowConfigurationIsFrozenException();
+ }
+ }
}
diff --git a/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs b/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs
index 149b51b..477fa4a 100644
--- a/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs
+++ b/src/DbConnectionPlus/Configuration/IEntityPropertyBuilder.cs
@@ -8,40 +8,40 @@ internal interface IEntityPropertyBuilder : IFreezable
///
/// The name of the column the property is mapped to.
///
- internal String? ColumnName { get; }
+ internal string? ColumnName { get; }
///
/// Determines whether the property is mapped to a computed database column.
///
- internal Boolean IsComputed { get; }
+ internal bool IsComputed { get; }
///
/// Determines whether the property participates in optimistic concurrency checks.
///
- internal Boolean IsConcurrencyToken { get; }
+ internal bool IsConcurrencyToken { get; }
///
/// Determines whether the property is mapped to an identity database column.
///
- internal Boolean IsIdentity { get; }
+ internal bool IsIdentity { get; }
///
/// Determines whether the property is not mapped to a database column.
///
- internal Boolean IsIgnored { get; }
+ internal bool IsIgnored { get; }
///
/// Determines whether the property is mapped to a key database column.
///
- internal Boolean IsKey { get; }
+ internal bool IsKey { get; }
///
/// Determines whether the property is a row version used for concurrency control.
///
- internal Boolean IsRowVersion { get; }
+ internal bool IsRowVersion { get; }
///
/// The name of the property being configured.
///
- internal String PropertyName { get; }
+ internal string PropertyName { get; }
}
diff --git a/src/DbConnectionPlus/Configuration/IEntityTypeBuilder.cs b/src/DbConnectionPlus/Configuration/IEntityTypeBuilder.cs
index cbaa2fe..4c36123 100644
--- a/src/DbConnectionPlus/Configuration/IEntityTypeBuilder.cs
+++ b/src/DbConnectionPlus/Configuration/IEntityTypeBuilder.cs
@@ -13,10 +13,10 @@ internal interface IEntityTypeBuilder : IFreezable
///
/// The property builders associated with the entity type.
///
- internal IReadOnlyDictionary PropertyBuilders { get; }
+ internal IReadOnlyDictionary PropertyBuilders { get; }
///
/// The name of the table the entity type is mapped to.
///
- internal String? TableName { get; }
+ internal string? TableName { get; }
}
diff --git a/src/DbConnectionPlus/Converters/EnumConverter.cs b/src/DbConnectionPlus/Converters/EnumConverter.cs
index 11b1db8..2c30baa 100644
--- a/src/DbConnectionPlus/Converters/EnumConverter.cs
+++ b/src/DbConnectionPlus/Converters/EnumConverter.cs
@@ -1,7 +1,6 @@
// Copyright (c) 2026 David Liebeherr
// Licensed under the MIT License. See LICENSE.md in the project root for more information.
-using System.Diagnostics.CodeAnalysis;
using RentADeveloper.DbConnectionPlus.Extensions;
namespace RentADeveloper.DbConnectionPlus.Converters;
@@ -82,7 +81,7 @@ internal static class EnumConverter
///
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static TTarget? ConvertValueToEnumMember(Object? value)
+ internal static TTarget? ConvertValueToEnumMember(object? value)
{
var targetType = typeof(TTarget);
@@ -99,18 +98,19 @@ internal static class EnumConverter
case null or DBNull when default(TTarget) is null:
return default;
- case null or DBNull when default(TTarget) is not null:
+ case null
+ or DBNull when default(TTarget) is not null:
ThrowCouldNotConvertNullToNonNullableEnumTypeException(targetType);
return default; // Just to satisfy the compiler.
case TTarget alreadyTargetTypeValue:
return alreadyTargetTypeValue;
- case String stringValue when String.IsNullOrWhiteSpace(stringValue):
+ case string stringValue when string.IsNullOrWhiteSpace(stringValue):
ThrowCouldNotConvertEmptyOrWhitespaceStringToEnumTypeException(targetType);
return default; // Just to satisfy the compiler.
- case String stringValue:
+ case string stringValue:
if (!Enum.TryParse(effectiveTargetType, stringValue, true, out var result))
{
ThrowCouldNotConvertStringToEnumTypeException(stringValue, targetType);
@@ -118,7 +118,17 @@ internal static class EnumConverter
return (TTarget?)result;
- case Byte or SByte or Int16 or UInt16 or Int32 or UInt32 or Int64 or UInt64 or Double or Single or Decimal:
+ case byte
+ or sbyte
+ or short
+ or ushort
+ or int
+ or uint
+ or long
+ or ulong
+ or double
+ or float
+ or decimal:
var enumUnderlyingType = Enum.GetUnderlyingType(effectiveTargetType);
var valueConvertedToEnumUnderlyingType = Convert.ChangeType(
@@ -132,16 +142,10 @@ internal static class EnumConverter
ThrowCouldNotConvertNumericValueToEnumType(value, targetType);
}
- return (TTarget?)Enum.ToObject(
- effectiveTargetType,
- valueConvertedToEnumUnderlyingType
- );
+ return (TTarget?)Enum.ToObject(effectiveTargetType, valueConvertedToEnumUnderlyingType);
default:
- ThrowValueIsNeitherEnumValueNorStringNorNumericValueException(
- value,
- targetType
- );
+ ThrowValueIsNeitherEnumValueNorStringNorNumericValueException(value, targetType);
return default; // Just to satisfy the compiler.
}
}
@@ -218,7 +222,7 @@ internal static class EnumConverter
///
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static Object? ConvertValueToEnumMember(Object? value, Type targetType)
+ internal static object? ConvertValueToEnumMember(object? value, Type targetType)
{
ArgumentNullException.ThrowIfNull(targetType);
@@ -235,18 +239,19 @@ internal static class EnumConverter
case null or DBNull when targetType.IsReferenceTypeOrNullableType():
return null;
- case null or DBNull when !targetType.IsReferenceTypeOrNullableType():
+ case null
+ or DBNull when !targetType.IsReferenceTypeOrNullableType():
ThrowCouldNotConvertNullToNonNullableEnumTypeException(targetType);
return null; // Just to satisfy the compiler.
case not null when value.GetType().IsAssignableTo(effectiveTargetType):
return value;
- case String stringValue when String.IsNullOrWhiteSpace(stringValue):
+ case string stringValue when string.IsNullOrWhiteSpace(stringValue):
ThrowCouldNotConvertEmptyOrWhitespaceStringToEnumTypeException(targetType);
return null; // Just to satisfy the compiler.
- case String stringValue:
+ case string stringValue:
if (!Enum.TryParse(effectiveTargetType, stringValue, true, out var result))
{
ThrowCouldNotConvertStringToEnumTypeException(stringValue, targetType);
@@ -254,7 +259,17 @@ internal static class EnumConverter
return result;
- case Byte or SByte or Int16 or UInt16 or Int32 or UInt32 or Int64 or UInt64 or Double or Single or Decimal:
+ case byte
+ or sbyte
+ or short
+ or ushort
+ or int
+ or uint
+ or long
+ or ulong
+ or double
+ or float
+ or decimal:
var enumUnderlyingType = Enum.GetUnderlyingType(effectiveTargetType);
var valueConvertedToEnumUnderlyingType = Convert.ChangeType(
@@ -268,16 +283,10 @@ internal static class EnumConverter
ThrowCouldNotConvertNumericValueToEnumType(value, targetType);
}
- return Enum.ToObject(
- effectiveTargetType,
- valueConvertedToEnumUnderlyingType
- );
+ return Enum.ToObject(effectiveTargetType, valueConvertedToEnumUnderlyingType);
default:
- ThrowValueIsNeitherEnumValueNorStringNorNumericValueException(
- value,
- targetType
- );
+ ThrowValueIsNeitherEnumValueNorStringNorNumericValueException(value, targetType);
return null; // Just to satisfy the compiler.
}
}
@@ -286,54 +295,48 @@ internal static class EnumConverter
[DoesNotReturn]
private static void ThrowCouldNotConvertEmptyOrWhitespaceStringToEnumTypeException(Type enumType) =>
throw new InvalidCastException(
- "Could not convert an empty string or a string that consists only of white-space characters to an enum " +
- $"member of the type {enumType}."
+ "Could not convert an empty string or a string that consists only of white-space characters to an enum "
+ + $"member of the type {enumType}."
);
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
private static void ThrowCouldNotConvertNullToNonNullableEnumTypeException(Type enumType) =>
- throw new InvalidCastException(
- $"Could not convert {{null}} to an enum member of the type {enumType}."
- );
+ throw new InvalidCastException($"Could not convert {{null}} to an enum member of the type {enumType}.");
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
- private static void
- ThrowCouldNotConvertNumericValueToEnumType(Object value, Type enumType) =>
+ private static void ThrowCouldNotConvertNumericValueToEnumType(object value, Type enumType) =>
throw new InvalidCastException(
- $"Could not convert the value {value.ToDebugString()} to an enum member of the type {enumType}. That " +
- "value does not match any of the values of the enum's members."
+ $"Could not convert the value {value.ToDebugString()} to an enum member of the type {enumType}. That "
+ + "value does not match any of the values of the enum's members."
);
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
- private static void ThrowCouldNotConvertStringToEnumTypeException(
- String value,
- Type enumType
- ) =>
+ private static void ThrowCouldNotConvertStringToEnumTypeException(string value, Type enumType) =>
throw new InvalidCastException(
- $"Could not convert the string '{value}' to an enum member of the type {enumType}. That string does " +
- "not match any of the names of the enum's members."
+ $"Could not convert the string '{value}' to an enum member of the type {enumType}. That string does "
+ + "not match any of the names of the enum's members."
);
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
- private static void ThrowTypeIsNeitherEnumNorNullableEnumTypeException(Object? value, Type enumType) =>
+ private static void ThrowTypeIsNeitherEnumNorNullableEnumTypeException(object? value, Type enumType) =>
throw new ArgumentException(
- $"Could not convert the value {value.ToDebugString()} to an enum member of the type {enumType}, because " +
- $"the type {enumType} is not an enum type.",
+ $"Could not convert the value {value.ToDebugString()} to an enum member of the type {enumType}, because "
+ + $"the type {enumType} is not an enum type.",
nameof(enumType)
);
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
private static void ThrowValueIsNeitherEnumValueNorStringNorNumericValueException(
- Object? value,
+ object? value,
Type originalEnumType
) =>
throw new InvalidCastException(
- $"Could not convert the value {value.ToDebugString()} to an enum member of the type {originalEnumType}. " +
- "The value must either be an enum value of that type or a string or a numeric value."
+ $"Could not convert the value {value.ToDebugString()} to an enum member of the type {originalEnumType}. "
+ + "The value must either be an enum value of that type or a string or a numeric value."
);
}
diff --git a/src/DbConnectionPlus/Converters/EnumSerializer.cs b/src/DbConnectionPlus/Converters/EnumSerializer.cs
index 48a1983..88f4cfa 100644
--- a/src/DbConnectionPlus/Converters/EnumSerializer.cs
+++ b/src/DbConnectionPlus/Converters/EnumSerializer.cs
@@ -21,20 +21,17 @@ internal static class EnumSerializer
/// is not a valid value.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static Object SerializeEnum(Enum enumValue, EnumSerializationMode serializationMode)
+ internal static object SerializeEnum(Enum enumValue, EnumSerializationMode serializationMode)
{
ArgumentNullException.ThrowIfNull(enumValue);
return serializationMode switch
{
- EnumSerializationMode.Strings =>
- enumValue.ToString(),
+ EnumSerializationMode.Strings => enumValue.ToString(),
- EnumSerializationMode.Integers =>
- Convert.ToInt32(enumValue, CultureInfo.InvariantCulture),
+ EnumSerializationMode.Integers => Convert.ToInt32(enumValue, CultureInfo.InvariantCulture),
- _ =>
- ThrowHelper.ThrowInvalidEnumSerializationModeException(serializationMode)
+ _ => ThrowHelper.ThrowInvalidEnumSerializationModeException(serializationMode),
};
}
}
diff --git a/src/DbConnectionPlus/Converters/ValueConverter.cs b/src/DbConnectionPlus/Converters/ValueConverter.cs
index 635be2a..f65c99c 100644
--- a/src/DbConnectionPlus/Converters/ValueConverter.cs
+++ b/src/DbConnectionPlus/Converters/ValueConverter.cs
@@ -1,7 +1,6 @@
// Copyright (c) 2026 David Liebeherr
// Licensed under the MIT License. See LICENSE.md in the project root for more information.
-using System.Diagnostics.CodeAnalysis;
using RentADeveloper.DbConnectionPlus.Extensions;
namespace RentADeveloper.DbConnectionPlus.Converters;
@@ -11,6 +10,222 @@ namespace RentADeveloper.DbConnectionPlus.Converters;
///
internal static class ValueConverter
{
+ private static readonly HashSet<(Type SourceType, Type TargetType)> supportedConversions =
+ [
+ (typeof(bool), typeof(bool)),
+ (typeof(bool), typeof(byte)),
+ (typeof(bool), typeof(decimal)),
+ (typeof(bool), typeof(double)),
+ (typeof(bool), typeof(short)),
+ (typeof(bool), typeof(int)),
+ (typeof(bool), typeof(long)),
+ (typeof(bool), typeof(sbyte)),
+ (typeof(bool), typeof(float)),
+ (typeof(bool), typeof(string)),
+ (typeof(bool), typeof(ushort)),
+ (typeof(bool), typeof(uint)),
+ (typeof(bool), typeof(ulong)),
+ (typeof(byte), typeof(bool)),
+ (typeof(byte), typeof(byte)),
+ (typeof(byte), typeof(char)),
+ (typeof(byte), typeof(decimal)),
+ (typeof(byte), typeof(double)),
+ (typeof(byte), typeof(short)),
+ (typeof(byte), typeof(int)),
+ (typeof(byte), typeof(long)),
+ (typeof(byte), typeof(sbyte)),
+ (typeof(byte), typeof(float)),
+ (typeof(byte), typeof(string)),
+ (typeof(byte), typeof(ushort)),
+ (typeof(byte), typeof(uint)),
+ (typeof(byte), typeof(ulong)),
+ (typeof(byte[]), typeof(Guid)),
+ (typeof(char), typeof(byte)),
+ (typeof(char), typeof(char)),
+ (typeof(char), typeof(short)),
+ (typeof(char), typeof(int)),
+ (typeof(char), typeof(long)),
+ (typeof(char), typeof(sbyte)),
+ (typeof(char), typeof(string)),
+ (typeof(char), typeof(ushort)),
+ (typeof(char), typeof(uint)),
+ (typeof(char), typeof(ulong)),
+ (typeof(DateOnly), typeof(DateOnly)),
+ (typeof(DateOnly), typeof(string)),
+ (typeof(DateTime), typeof(DateTime)),
+ (typeof(DateTime), typeof(DateOnly)),
+ (typeof(DateTime), typeof(string)),
+ (typeof(DateTimeOffset), typeof(DateTimeOffset)),
+ (typeof(DateTimeOffset), typeof(string)),
+ (typeof(decimal), typeof(bool)),
+ (typeof(decimal), typeof(byte)),
+ (typeof(decimal), typeof(decimal)),
+ (typeof(decimal), typeof(double)),
+ (typeof(decimal), typeof(short)),
+ (typeof(decimal), typeof(int)),
+ (typeof(decimal), typeof(long)),
+ (typeof(decimal), typeof(sbyte)),
+ (typeof(decimal), typeof(float)),
+ (typeof(decimal), typeof(string)),
+ (typeof(decimal), typeof(ushort)),
+ (typeof(decimal), typeof(uint)),
+ (typeof(decimal), typeof(ulong)),
+ (typeof(double), typeof(bool)),
+ (typeof(double), typeof(byte)),
+ (typeof(double), typeof(decimal)),
+ (typeof(double), typeof(double)),
+ (typeof(double), typeof(short)),
+ (typeof(double), typeof(int)),
+ (typeof(double), typeof(long)),
+ (typeof(double), typeof(sbyte)),
+ (typeof(double), typeof(float)),
+ (typeof(double), typeof(string)),
+ (typeof(double), typeof(ushort)),
+ (typeof(double), typeof(uint)),
+ (typeof(double), typeof(ulong)),
+ (typeof(Guid), typeof(byte[])),
+ (typeof(Guid), typeof(Guid)),
+ (typeof(Guid), typeof(string)),
+ (typeof(short), typeof(bool)),
+ (typeof(short), typeof(byte)),
+ (typeof(short), typeof(char)),
+ (typeof(short), typeof(decimal)),
+ (typeof(short), typeof(double)),
+ (typeof(short), typeof(short)),
+ (typeof(short), typeof(int)),
+ (typeof(short), typeof(long)),
+ (typeof(short), typeof(sbyte)),
+ (typeof(short), typeof(float)),
+ (typeof(short), typeof(string)),
+ (typeof(short), typeof(ushort)),
+ (typeof(short), typeof(uint)),
+ (typeof(short), typeof(ulong)),
+ (typeof(int), typeof(bool)),
+ (typeof(int), typeof(byte)),
+ (typeof(int), typeof(char)),
+ (typeof(int), typeof(decimal)),
+ (typeof(int), typeof(double)),
+ (typeof(int), typeof(short)),
+ (typeof(int), typeof(int)),
+ (typeof(int), typeof(long)),
+ (typeof(int), typeof(sbyte)),
+ (typeof(int), typeof(float)),
+ (typeof(int), typeof(string)),
+ (typeof(int), typeof(ushort)),
+ (typeof(int), typeof(uint)),
+ (typeof(int), typeof(ulong)),
+ (typeof(long), typeof(bool)),
+ (typeof(long), typeof(byte)),
+ (typeof(long), typeof(char)),
+ (typeof(long), typeof(decimal)),
+ (typeof(long), typeof(double)),
+ (typeof(long), typeof(short)),
+ (typeof(long), typeof(int)),
+ (typeof(long), typeof(long)),
+ (typeof(long), typeof(sbyte)),
+ (typeof(long), typeof(float)),
+ (typeof(long), typeof(string)),
+ (typeof(long), typeof(ushort)),
+ (typeof(long), typeof(uint)),
+ (typeof(long), typeof(ulong)),
+ (typeof(nint), typeof(nint)),
+ (typeof(sbyte), typeof(bool)),
+ (typeof(sbyte), typeof(byte)),
+ (typeof(sbyte), typeof(char)),
+ (typeof(sbyte), typeof(decimal)),
+ (typeof(sbyte), typeof(double)),
+ (typeof(sbyte), typeof(short)),
+ (typeof(sbyte), typeof(int)),
+ (typeof(sbyte), typeof(long)),
+ (typeof(sbyte), typeof(sbyte)),
+ (typeof(sbyte), typeof(float)),
+ (typeof(sbyte), typeof(string)),
+ (typeof(sbyte), typeof(ushort)),
+ (typeof(sbyte), typeof(uint)),
+ (typeof(sbyte), typeof(ulong)),
+ (typeof(float), typeof(bool)),
+ (typeof(float), typeof(byte)),
+ (typeof(float), typeof(decimal)),
+ (typeof(float), typeof(double)),
+ (typeof(float), typeof(short)),
+ (typeof(float), typeof(int)),
+ (typeof(float), typeof(long)),
+ (typeof(float), typeof(sbyte)),
+ (typeof(float), typeof(float)),
+ (typeof(float), typeof(string)),
+ (typeof(float), typeof(ushort)),
+ (typeof(float), typeof(uint)),
+ (typeof(float), typeof(ulong)),
+ (typeof(string), typeof(bool)),
+ (typeof(string), typeof(byte)),
+ (typeof(string), typeof(char)),
+ (typeof(string), typeof(DateTime)),
+ (typeof(string), typeof(DateTimeOffset)),
+ (typeof(string), typeof(DateOnly)),
+ (typeof(string), typeof(decimal)),
+ (typeof(string), typeof(double)),
+ (typeof(string), typeof(Guid)),
+ (typeof(string), typeof(short)),
+ (typeof(string), typeof(int)),
+ (typeof(string), typeof(long)),
+ (typeof(string), typeof(sbyte)),
+ (typeof(string), typeof(float)),
+ (typeof(string), typeof(string)),
+ (typeof(string), typeof(ushort)),
+ (typeof(string), typeof(uint)),
+ (typeof(string), typeof(ulong)),
+ (typeof(string), typeof(TimeSpan)),
+ (typeof(string), typeof(TimeOnly)),
+ (typeof(TimeOnly), typeof(TimeOnly)),
+ (typeof(TimeOnly), typeof(string)),
+ (typeof(TimeSpan), typeof(TimeOnly)),
+ (typeof(TimeSpan), typeof(TimeSpan)),
+ (typeof(TimeSpan), typeof(string)),
+ (typeof(ushort), typeof(bool)),
+ (typeof(ushort), typeof(byte)),
+ (typeof(ushort), typeof(char)),
+ (typeof(ushort), typeof(decimal)),
+ (typeof(ushort), typeof(double)),
+ (typeof(ushort), typeof(short)),
+ (typeof(ushort), typeof(int)),
+ (typeof(ushort), typeof(long)),
+ (typeof(ushort), typeof(sbyte)),
+ (typeof(ushort), typeof(float)),
+ (typeof(ushort), typeof(string)),
+ (typeof(ushort), typeof(ushort)),
+ (typeof(ushort), typeof(uint)),
+ (typeof(ushort), typeof(ulong)),
+ (typeof(uint), typeof(bool)),
+ (typeof(uint), typeof(byte)),
+ (typeof(uint), typeof(char)),
+ (typeof(uint), typeof(decimal)),
+ (typeof(uint), typeof(double)),
+ (typeof(uint), typeof(short)),
+ (typeof(uint), typeof(int)),
+ (typeof(uint), typeof(long)),
+ (typeof(uint), typeof(sbyte)),
+ (typeof(uint), typeof(float)),
+ (typeof(uint), typeof(string)),
+ (typeof(uint), typeof(ushort)),
+ (typeof(uint), typeof(uint)),
+ (typeof(uint), typeof(ulong)),
+ (typeof(ulong), typeof(bool)),
+ (typeof(ulong), typeof(byte)),
+ (typeof(ulong), typeof(char)),
+ (typeof(ulong), typeof(decimal)),
+ (typeof(ulong), typeof(double)),
+ (typeof(ulong), typeof(short)),
+ (typeof(ulong), typeof(int)),
+ (typeof(ulong), typeof(long)),
+ (typeof(ulong), typeof(sbyte)),
+ (typeof(ulong), typeof(float)),
+ (typeof(ulong), typeof(string)),
+ (typeof(ulong), typeof(ushort)),
+ (typeof(ulong), typeof(uint)),
+ (typeof(ulong), typeof(ulong)),
+ (typeof(nuint), typeof(nuint)),
+ ];
+
///
/// Determines whether this converter can convert a value of the type to the type
/// .
@@ -35,7 +250,7 @@ internal static class ValueConverter
///
///
///
- internal static Boolean CanConvert(Type sourceType, Type targetType)
+ internal static bool CanConvert(Type sourceType, Type targetType)
{
ArgumentNullException.ThrowIfNull(sourceType);
ArgumentNullException.ThrowIfNull(targetType);
@@ -43,10 +258,7 @@ internal static Boolean CanConvert(Type sourceType, Type targetType)
var effectiveSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
var effectiveTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
- if (
- effectiveSourceType == effectiveTargetType ||
- effectiveTargetType == typeof(Object)
- )
+ if (effectiveSourceType == effectiveTargetType || effectiveTargetType == typeof(object))
{
// Conversion to same type or to Object is always possible.
return true;
@@ -87,14 +299,14 @@ internal static Boolean CanConvert(Type sourceType, Type targetType)
///
///
///
- /// is or and
+ /// is or and
/// is a string that has a length other than 1.
///
///
///
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static TTarget? ConvertValueToType(Object? value)
+ internal static TTarget? ConvertValueToType(object? value)
{
var targetType = typeof(TTarget);
@@ -108,93 +320,91 @@ internal static Boolean CanConvert(Type sourceType, Type targetType)
case null or DBNull when default(TTarget) is null:
return default;
- case null or DBNull when default(TTarget) is not null:
+ case null
+ or DBNull when default(TTarget) is not null:
ThrowCouldNotConvertNullOrDbNullToNonNullableTargetTypeException(value, targetType);
return default; // Just to satisfy the compiler.
case TTarget alreadyTargetTypeValue:
return alreadyTargetTypeValue;
- case String stringValue when effectiveTargetType == typeof(Guid):
+ case string stringValue when effectiveTargetType == typeof(Guid):
if (!Guid.TryParse(stringValue, out var guidResult))
{
ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType);
}
- return (TTarget)(Object)guidResult;
+ return (TTarget)(object)guidResult;
- case String stringValue when effectiveTargetType == typeof(TimeSpan):
+ case string stringValue when effectiveTargetType == typeof(TimeSpan):
if (!TimeSpan.TryParse(stringValue, CultureInfo.InvariantCulture, out var timeSpanResult))
{
ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType);
}
- return (TTarget)(Object)timeSpanResult;
+ return (TTarget)(object)timeSpanResult;
- case String stringValue when effectiveTargetType == typeof(Char):
+ case string stringValue when effectiveTargetType == typeof(char):
if (stringValue.Length != 1)
{
- ThrowCouldNotConvertNonSingleCharStringToCharException(
- stringValue,
- targetType
- );
+ ThrowCouldNotConvertNonSingleCharStringToCharException(stringValue, targetType);
}
- return (TTarget)(Object)stringValue[0];
+ return (TTarget)(object)stringValue[0];
- case String stringValue when effectiveTargetType == typeof(DateTimeOffset):
+ case string stringValue when effectiveTargetType == typeof(DateTimeOffset):
if (!DateTimeOffset.TryParse(stringValue, CultureInfo.InvariantCulture, out var dateTimeOffsetResult))
{
ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType);
}
- return (TTarget)(Object)dateTimeOffsetResult;
+ return (TTarget)(object)dateTimeOffsetResult;
- case String stringValue when effectiveTargetType == typeof(DateOnly):
+ case string stringValue when effectiveTargetType == typeof(DateOnly):
if (!DateOnly.TryParse(stringValue, CultureInfo.InvariantCulture, out var dateOnlyResult))
{
ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType);
}
- return (TTarget)(Object)dateOnlyResult;
+ return (TTarget)(object)dateOnlyResult;
- case String stringValue when effectiveTargetType == typeof(TimeOnly):
+ case string stringValue when effectiveTargetType == typeof(TimeOnly):
if (!TimeOnly.TryParse(stringValue, CultureInfo.InvariantCulture, out var timeOnlyResult))
{
ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType);
}
- return (TTarget)(Object)timeOnlyResult;
+ return (TTarget)(object)timeOnlyResult;
- case Guid guid when targetType == typeof(String):
- return (TTarget)(Object)guid.ToString("D");
+ case Guid guid when targetType == typeof(string):
+ return (TTarget)(object)guid.ToString("D");
- case Guid guid when targetType == typeof(Byte[]):
- return (TTarget)(Object)guid.ToByteArray();
+ case Guid guid when targetType == typeof(byte[]):
+ return (TTarget)(object)guid.ToByteArray();
- case DateTime dateTime when targetType == typeof(String):
- return (TTarget)(Object)dateTime.ToString("O", CultureInfo.InvariantCulture);
+ case DateTime dateTime when targetType == typeof(string):
+ return (TTarget)(object)dateTime.ToString("O", CultureInfo.InvariantCulture);
case DateTime dateTime when effectiveTargetType == typeof(DateOnly):
- return (TTarget)(Object)DateOnly.FromDateTime(dateTime);
+ return (TTarget)(object)DateOnly.FromDateTime(dateTime);
- case TimeSpan timeSpan when targetType == typeof(String):
- return (TTarget)(Object)timeSpan.ToString("g", CultureInfo.InvariantCulture);
+ case TimeSpan timeSpan when targetType == typeof(string):
+ return (TTarget)(object)timeSpan.ToString("g", CultureInfo.InvariantCulture);
case TimeSpan timeSpan when effectiveTargetType == typeof(TimeOnly):
- return (TTarget)(Object)TimeOnly.FromTimeSpan(timeSpan);
+ return (TTarget)(object)TimeOnly.FromTimeSpan(timeSpan);
- case Byte[] bytes when effectiveTargetType == typeof(Guid):
- return (TTarget)(Object)new Guid(bytes);
+ case byte[] bytes when effectiveTargetType == typeof(Guid):
+ return (TTarget)(object)new Guid(bytes);
- case DateTimeOffset dateTimeOffset when targetType == typeof(String):
- return (TTarget)(Object)dateTimeOffset.ToString("O", CultureInfo.InvariantCulture);
+ case DateTimeOffset dateTimeOffset when targetType == typeof(string):
+ return (TTarget)(object)dateTimeOffset.ToString("O", CultureInfo.InvariantCulture);
- case DateOnly dateOnly when targetType == typeof(String):
- return (TTarget)(Object)dateOnly.ToString("O", CultureInfo.InvariantCulture);
+ case DateOnly dateOnly when targetType == typeof(string):
+ return (TTarget)(object)dateOnly.ToString("O", CultureInfo.InvariantCulture);
- case TimeOnly timeOnly when targetType == typeof(String):
- return (TTarget)(Object)timeOnly.ToString("O", CultureInfo.InvariantCulture);
+ case TimeOnly timeOnly when targetType == typeof(string):
+ return (TTarget)(object)timeOnly.ToString("O", CultureInfo.InvariantCulture);
default:
if (effectiveTargetType.IsEnum)
@@ -204,21 +414,13 @@ internal static Boolean CanConvert(Type sourceType, Type targetType)
try
{
- return (TTarget?)Convert.ChangeType(
- value,
- effectiveTargetType,
- CultureInfo.InvariantCulture
- );
+ return (TTarget?)Convert.ChangeType(value, effectiveTargetType, CultureInfo.InvariantCulture);
}
- catch (Exception exception) when (
- exception is ArgumentException or InvalidCastException or FormatException or OverflowException
- )
+ catch (Exception exception)
+ when (exception is ArgumentException or InvalidCastException or FormatException or OverflowException
+ )
{
- ThrowCouldNotConvertValueToTargetTypeException(
- value,
- targetType,
- exception
- );
+ ThrowCouldNotConvertValueToTargetTypeException(value, targetType, exception);
return default; // Just to satisfy the compiler
}
}
@@ -247,14 +449,14 @@ exception is ArgumentException or InvalidCastException or FormatException or Ove
///
///
///
- /// is or and
+ /// is or and
/// is a string that has a length other than 1.
///
///
///
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static Object? ConvertValueToType(Object? value, Type targetType)
+ internal static object? ConvertValueToType(object? value, Type targetType)
{
ArgumentNullException.ThrowIfNull(targetType);
@@ -268,14 +470,15 @@ exception is ArgumentException or InvalidCastException or FormatException or Ove
case null or DBNull when targetType.IsReferenceTypeOrNullableType():
return null;
- case null or DBNull when !targetType.IsReferenceTypeOrNullableType():
+ case null
+ or DBNull when !targetType.IsReferenceTypeOrNullableType():
ThrowCouldNotConvertNullOrDbNullToNonNullableTargetTypeException(value, targetType);
return null; // Just to satisfy the compiler.
case not null when value.GetType().IsAssignableTo(effectiveTargetType):
return value;
- case String stringValue when effectiveTargetType == typeof(Guid):
+ case string stringValue when effectiveTargetType == typeof(Guid):
if (!Guid.TryParse(stringValue, out var guidResult))
{
ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType);
@@ -283,7 +486,7 @@ exception is ArgumentException or InvalidCastException or FormatException or Ove
return guidResult;
- case String stringValue when effectiveTargetType == typeof(TimeSpan):
+ case string stringValue when effectiveTargetType == typeof(TimeSpan):
if (!TimeSpan.TryParse(stringValue, CultureInfo.InvariantCulture, out var timeSpanResult))
{
ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType);
@@ -291,18 +494,15 @@ exception is ArgumentException or InvalidCastException or FormatException or Ove
return timeSpanResult;
- case String stringValue when effectiveTargetType == typeof(Char):
+ case string stringValue when effectiveTargetType == typeof(char):
if (stringValue.Length != 1)
{
- ThrowCouldNotConvertNonSingleCharStringToCharException(
- stringValue,
- targetType
- );
+ ThrowCouldNotConvertNonSingleCharStringToCharException(stringValue, targetType);
}
return stringValue[0];
- case String stringValue when effectiveTargetType == typeof(DateTimeOffset):
+ case string stringValue when effectiveTargetType == typeof(DateTimeOffset):
if (!DateTimeOffset.TryParse(stringValue, CultureInfo.InvariantCulture, out var dateTimeOffsetResult))
{
ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType);
@@ -310,7 +510,7 @@ exception is ArgumentException or InvalidCastException or FormatException or Ove
return dateTimeOffsetResult;
- case String stringValue when effectiveTargetType == typeof(DateOnly):
+ case string stringValue when effectiveTargetType == typeof(DateOnly):
if (!DateOnly.TryParse(stringValue, CultureInfo.InvariantCulture, out var dateOnlyResult))
{
ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType);
@@ -318,7 +518,7 @@ exception is ArgumentException or InvalidCastException or FormatException or Ove
return dateOnlyResult;
- case String stringValue when effectiveTargetType == typeof(TimeOnly):
+ case string stringValue when effectiveTargetType == typeof(TimeOnly):
if (!TimeOnly.TryParse(stringValue, CultureInfo.InvariantCulture, out var timeOnlyResult))
{
ThrowCouldNotConvertValueToTargetTypeException(stringValue, targetType);
@@ -326,34 +526,34 @@ exception is ArgumentException or InvalidCastException or FormatException or Ove
return timeOnlyResult;
- case Guid guid when targetType == typeof(String):
+ case Guid guid when targetType == typeof(string):
return guid.ToString("D");
- case Guid guid when targetType == typeof(Byte[]):
+ case Guid guid when targetType == typeof(byte[]):
return guid.ToByteArray();
- case DateTime dateTime when targetType == typeof(String):
+ case DateTime dateTime when targetType == typeof(string):
return dateTime.ToString("O", CultureInfo.InvariantCulture);
case DateTime dateTime when effectiveTargetType == typeof(DateOnly):
return DateOnly.FromDateTime(dateTime);
- case TimeSpan timeSpan when targetType == typeof(String):
+ case TimeSpan timeSpan when targetType == typeof(string):
return timeSpan.ToString("g", CultureInfo.InvariantCulture);
case TimeSpan timeSpan when effectiveTargetType == typeof(TimeOnly):
return TimeOnly.FromTimeSpan(timeSpan);
- case Byte[] bytes when effectiveTargetType == typeof(Guid):
+ case byte[] bytes when effectiveTargetType == typeof(Guid):
return new Guid(bytes);
- case DateTimeOffset dateTimeOffset when targetType == typeof(String):
+ case DateTimeOffset dateTimeOffset when targetType == typeof(string):
return dateTimeOffset.ToString("O", CultureInfo.InvariantCulture);
- case DateOnly dateOnly when targetType == typeof(String):
+ case DateOnly dateOnly when targetType == typeof(string):
return dateOnly.ToString("O", CultureInfo.InvariantCulture);
- case TimeOnly timeOnly when targetType == typeof(String):
+ case TimeOnly timeOnly when targetType == typeof(string):
return timeOnly.ToString("O", CultureInfo.InvariantCulture);
default:
@@ -364,15 +564,11 @@ exception is ArgumentException or InvalidCastException or FormatException or Ove
try
{
- return Convert.ChangeType(
- value,
- effectiveTargetType,
- CultureInfo.InvariantCulture
- );
+ return Convert.ChangeType(value, effectiveTargetType, CultureInfo.InvariantCulture);
}
- catch (Exception exception) when (
- exception is ArgumentException or InvalidCastException or FormatException or OverflowException
- )
+ catch (Exception exception)
+ when (exception is ArgumentException or InvalidCastException or FormatException or OverflowException
+ )
{
ThrowCouldNotConvertValueToTargetTypeException(value, targetType, exception);
return null; // Just to satisfy the compiler
@@ -389,299 +585,59 @@ exception is ArgumentException or InvalidCastException or FormatException or Ove
/// if is a type that can be converted to an enum type or a type
/// that an enum can be converted to; otherwise, .
///
- private static Boolean IsSupportedEnumConversionType(Type type) =>
- Type.GetTypeCode(type) is
- // Ordered by frequency of use:
- TypeCode.String or
- TypeCode.Int32 or
- TypeCode.Int16 or
- TypeCode.Int64 or
- TypeCode.Double or
- TypeCode.Single or
- TypeCode.Decimal or
- TypeCode.Byte or
- TypeCode.SByte or
- TypeCode.UInt16 or
- TypeCode.UInt32 or
- TypeCode.UInt64;
+ private static bool IsSupportedEnumConversionType(Type type) =>
+ Type.GetTypeCode(type)
+ is
+ // Ordered by frequency of use:
+ TypeCode.String
+ or TypeCode.Int32
+ or TypeCode.Int16
+ or TypeCode.Int64
+ or TypeCode.Double
+ or TypeCode.Single
+ or TypeCode.Decimal
+ or TypeCode.Byte
+ or TypeCode.SByte
+ or TypeCode.UInt16
+ or TypeCode.UInt32
+ or TypeCode.UInt64;
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
- private static void ThrowCouldNotConvertNonSingleCharStringToCharException(String stringValue, Type targetType) =>
+ private static void ThrowCouldNotConvertNonSingleCharStringToCharException(string stringValue, Type targetType) =>
throw new InvalidCastException(
- $"Could not convert the string '{stringValue}' to the type {targetType}. The string must be exactly one " +
- "character long."
+ $"Could not convert the string '{stringValue}' to the type {targetType}. The string must be exactly one "
+ + "character long."
);
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
private static void ThrowCouldNotConvertNullOrDbNullToNonNullableTargetTypeException(
- Object? value,
+ object? value,
Type targetType
) =>
throw new InvalidCastException(
- $"Could not convert the value {value.ToDebugString()} to the type {targetType}, because the type is " +
- "non-nullable."
+ $"Could not convert the value {value.ToDebugString()} to the type {targetType}, because the type is "
+ + "non-nullable."
);
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
private static void ThrowCouldNotConvertValueToTargetTypeException(
- Object? value,
+ object? value,
Type targetType,
Exception innerException
) =>
throw new InvalidCastException(
- $"Could not convert the value {value.ToDebugString()} to the type {targetType}. See inner exception " +
- "for details.",
+ $"Could not convert the value {value.ToDebugString()} to the type {targetType}. See inner exception "
+ + "for details.",
innerException
);
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
- private static void ThrowCouldNotConvertValueToTargetTypeException(
- Object? value,
- Type targetType
- ) =>
+ private static void ThrowCouldNotConvertValueToTargetTypeException(object? value, Type targetType) =>
throw new InvalidCastException(
$"Could not convert the value {value.ToDebugString()} to the type {targetType}. "
);
-
- private static readonly HashSet<(Type SourceType, Type TargetType)> supportedConversions =
- [
- (typeof(Boolean), typeof(Boolean)),
- (typeof(Boolean), typeof(Byte)),
- (typeof(Boolean), typeof(Decimal)),
- (typeof(Boolean), typeof(Double)),
- (typeof(Boolean), typeof(Int16)),
- (typeof(Boolean), typeof(Int32)),
- (typeof(Boolean), typeof(Int64)),
- (typeof(Boolean), typeof(SByte)),
- (typeof(Boolean), typeof(Single)),
- (typeof(Boolean), typeof(String)),
- (typeof(Boolean), typeof(UInt16)),
- (typeof(Boolean), typeof(UInt32)),
- (typeof(Boolean), typeof(UInt64)),
-
- (typeof(Byte), typeof(Boolean)),
- (typeof(Byte), typeof(Byte)),
- (typeof(Byte), typeof(Char)),
- (typeof(Byte), typeof(Decimal)),
- (typeof(Byte), typeof(Double)),
- (typeof(Byte), typeof(Int16)),
- (typeof(Byte), typeof(Int32)),
- (typeof(Byte), typeof(Int64)),
- (typeof(Byte), typeof(SByte)),
- (typeof(Byte), typeof(Single)),
- (typeof(Byte), typeof(String)),
- (typeof(Byte), typeof(UInt16)),
- (typeof(Byte), typeof(UInt32)),
- (typeof(Byte), typeof(UInt64)),
-
- (typeof(Byte[]), typeof(Guid)),
-
- (typeof(Char), typeof(Byte)),
- (typeof(Char), typeof(Char)),
- (typeof(Char), typeof(Int16)),
- (typeof(Char), typeof(Int32)),
- (typeof(Char), typeof(Int64)),
- (typeof(Char), typeof(SByte)),
- (typeof(Char), typeof(String)),
- (typeof(Char), typeof(UInt16)),
- (typeof(Char), typeof(UInt32)),
- (typeof(Char), typeof(UInt64)),
-
- (typeof(DateOnly), typeof(DateOnly)),
- (typeof(DateOnly), typeof(String)),
-
- (typeof(DateTime), typeof(DateTime)),
- (typeof(DateTime), typeof(DateOnly)),
- (typeof(DateTime), typeof(String)),
-
- (typeof(DateTimeOffset), typeof(DateTimeOffset)),
- (typeof(DateTimeOffset), typeof(String)),
-
- (typeof(Decimal), typeof(Boolean)),
- (typeof(Decimal), typeof(Byte)),
- (typeof(Decimal), typeof(Decimal)),
- (typeof(Decimal), typeof(Double)),
- (typeof(Decimal), typeof(Int16)),
- (typeof(Decimal), typeof(Int32)),
- (typeof(Decimal), typeof(Int64)),
- (typeof(Decimal), typeof(SByte)),
- (typeof(Decimal), typeof(Single)),
- (typeof(Decimal), typeof(String)),
- (typeof(Decimal), typeof(UInt16)),
- (typeof(Decimal), typeof(UInt32)),
- (typeof(Decimal), typeof(UInt64)),
-
- (typeof(Double), typeof(Boolean)),
- (typeof(Double), typeof(Byte)),
- (typeof(Double), typeof(Decimal)),
- (typeof(Double), typeof(Double)),
- (typeof(Double), typeof(Int16)),
- (typeof(Double), typeof(Int32)),
- (typeof(Double), typeof(Int64)),
- (typeof(Double), typeof(SByte)),
- (typeof(Double), typeof(Single)),
- (typeof(Double), typeof(String)),
- (typeof(Double), typeof(UInt16)),
- (typeof(Double), typeof(UInt32)),
- (typeof(Double), typeof(UInt64)),
-
- (typeof(Guid), typeof(Byte[])),
- (typeof(Guid), typeof(Guid)),
- (typeof(Guid), typeof(String)),
-
- (typeof(Int16), typeof(Boolean)),
- (typeof(Int16), typeof(Byte)),
- (typeof(Int16), typeof(Char)),
- (typeof(Int16), typeof(Decimal)),
- (typeof(Int16), typeof(Double)),
- (typeof(Int16), typeof(Int16)),
- (typeof(Int16), typeof(Int32)),
- (typeof(Int16), typeof(Int64)),
- (typeof(Int16), typeof(SByte)),
- (typeof(Int16), typeof(Single)),
- (typeof(Int16), typeof(String)),
- (typeof(Int16), typeof(UInt16)),
- (typeof(Int16), typeof(UInt32)),
- (typeof(Int16), typeof(UInt64)),
-
- (typeof(Int32), typeof(Boolean)),
- (typeof(Int32), typeof(Byte)),
- (typeof(Int32), typeof(Char)),
- (typeof(Int32), typeof(Decimal)),
- (typeof(Int32), typeof(Double)),
- (typeof(Int32), typeof(Int16)),
- (typeof(Int32), typeof(Int32)),
- (typeof(Int32), typeof(Int64)),
- (typeof(Int32), typeof(SByte)),
- (typeof(Int32), typeof(Single)),
- (typeof(Int32), typeof(String)),
- (typeof(Int32), typeof(UInt16)),
- (typeof(Int32), typeof(UInt32)),
- (typeof(Int32), typeof(UInt64)),
-
- (typeof(Int64), typeof(Boolean)),
- (typeof(Int64), typeof(Byte)),
- (typeof(Int64), typeof(Char)),
- (typeof(Int64), typeof(Decimal)),
- (typeof(Int64), typeof(Double)),
- (typeof(Int64), typeof(Int16)),
- (typeof(Int64), typeof(Int32)),
- (typeof(Int64), typeof(Int64)),
- (typeof(Int64), typeof(SByte)),
- (typeof(Int64), typeof(Single)),
- (typeof(Int64), typeof(String)),
- (typeof(Int64), typeof(UInt16)),
- (typeof(Int64), typeof(UInt32)),
- (typeof(Int64), typeof(UInt64)),
-
- (typeof(IntPtr), typeof(IntPtr)),
-
- (typeof(SByte), typeof(Boolean)),
- (typeof(SByte), typeof(Byte)),
- (typeof(SByte), typeof(Char)),
- (typeof(SByte), typeof(Decimal)),
- (typeof(SByte), typeof(Double)),
- (typeof(SByte), typeof(Int16)),
- (typeof(SByte), typeof(Int32)),
- (typeof(SByte), typeof(Int64)),
- (typeof(SByte), typeof(SByte)),
- (typeof(SByte), typeof(Single)),
- (typeof(SByte), typeof(String)),
- (typeof(SByte), typeof(UInt16)),
- (typeof(SByte), typeof(UInt32)),
- (typeof(SByte), typeof(UInt64)),
-
- (typeof(Single), typeof(Boolean)),
- (typeof(Single), typeof(Byte)),
- (typeof(Single), typeof(Decimal)),
- (typeof(Single), typeof(Double)),
- (typeof(Single), typeof(Int16)),
- (typeof(Single), typeof(Int32)),
- (typeof(Single), typeof(Int64)),
- (typeof(Single), typeof(SByte)),
- (typeof(Single), typeof(Single)),
- (typeof(Single), typeof(String)),
- (typeof(Single), typeof(UInt16)),
- (typeof(Single), typeof(UInt32)),
- (typeof(Single), typeof(UInt64)),
-
- (typeof(String), typeof(Boolean)),
- (typeof(String), typeof(Byte)),
- (typeof(String), typeof(Char)),
- (typeof(String), typeof(DateTime)),
- (typeof(String), typeof(DateTimeOffset)),
- (typeof(String), typeof(DateOnly)),
- (typeof(String), typeof(Decimal)),
- (typeof(String), typeof(Double)),
- (typeof(String), typeof(Guid)),
- (typeof(String), typeof(Int16)),
- (typeof(String), typeof(Int32)),
- (typeof(String), typeof(Int64)),
- (typeof(String), typeof(SByte)),
- (typeof(String), typeof(Single)),
- (typeof(String), typeof(String)),
- (typeof(String), typeof(UInt16)),
- (typeof(String), typeof(UInt32)),
- (typeof(String), typeof(UInt64)),
- (typeof(String), typeof(TimeSpan)),
- (typeof(String), typeof(TimeOnly)),
-
- (typeof(TimeOnly), typeof(TimeOnly)),
- (typeof(TimeOnly), typeof(String)),
-
- (typeof(TimeSpan), typeof(TimeOnly)),
- (typeof(TimeSpan), typeof(TimeSpan)),
- (typeof(TimeSpan), typeof(String)),
-
- (typeof(UInt16), typeof(Boolean)),
- (typeof(UInt16), typeof(Byte)),
- (typeof(UInt16), typeof(Char)),
- (typeof(UInt16), typeof(Decimal)),
- (typeof(UInt16), typeof(Double)),
- (typeof(UInt16), typeof(Int16)),
- (typeof(UInt16), typeof(Int32)),
- (typeof(UInt16), typeof(Int64)),
- (typeof(UInt16), typeof(SByte)),
- (typeof(UInt16), typeof(Single)),
- (typeof(UInt16), typeof(String)),
- (typeof(UInt16), typeof(UInt16)),
- (typeof(UInt16), typeof(UInt32)),
- (typeof(UInt16), typeof(UInt64)),
-
- (typeof(UInt32), typeof(Boolean)),
- (typeof(UInt32), typeof(Byte)),
- (typeof(UInt32), typeof(Char)),
- (typeof(UInt32), typeof(Decimal)),
- (typeof(UInt32), typeof(Double)),
- (typeof(UInt32), typeof(Int16)),
- (typeof(UInt32), typeof(Int32)),
- (typeof(UInt32), typeof(Int64)),
- (typeof(UInt32), typeof(SByte)),
- (typeof(UInt32), typeof(Single)),
- (typeof(UInt32), typeof(String)),
- (typeof(UInt32), typeof(UInt16)),
- (typeof(UInt32), typeof(UInt32)),
- (typeof(UInt32), typeof(UInt64)),
-
- (typeof(UInt64), typeof(Boolean)),
- (typeof(UInt64), typeof(Byte)),
- (typeof(UInt64), typeof(Char)),
- (typeof(UInt64), typeof(Decimal)),
- (typeof(UInt64), typeof(Double)),
- (typeof(UInt64), typeof(Int16)),
- (typeof(UInt64), typeof(Int32)),
- (typeof(UInt64), typeof(Int64)),
- (typeof(UInt64), typeof(SByte)),
- (typeof(UInt64), typeof(Single)),
- (typeof(UInt64), typeof(String)),
- (typeof(UInt64), typeof(UInt16)),
- (typeof(UInt64), typeof(UInt32)),
- (typeof(UInt64), typeof(UInt64)),
-
- (typeof(UIntPtr), typeof(UIntPtr))
- ];
}
diff --git a/src/DbConnectionPlus/DatabaseAdapters/Constants.cs b/src/DbConnectionPlus/DatabaseAdapters/Constants.cs
index 4b796ec..0e24630 100644
--- a/src/DbConnectionPlus/DatabaseAdapters/Constants.cs
+++ b/src/DbConnectionPlus/DatabaseAdapters/Constants.cs
@@ -11,10 +11,10 @@ public static class Constants
///
/// The string to use to indent parts of SQL statements.
///
- public const String Indent = " ";
+ public const string Indent = " ";
///
/// The name to use for the single column of single column temporary tables.
///
- public const String SingleColumnTemporaryTableColumnName = "Value";
+ public const string SingleColumnTemporaryTableColumnName = "Value";
}
diff --git a/src/DbConnectionPlus/DatabaseAdapters/IDatabaseAdapter.cs b/src/DbConnectionPlus/DatabaseAdapters/IDatabaseAdapter.cs
index f62cb9d..9b28dd1 100644
--- a/src/DbConnectionPlus/DatabaseAdapters/IDatabaseAdapter.cs
+++ b/src/DbConnectionPlus/DatabaseAdapters/IDatabaseAdapter.cs
@@ -30,7 +30,7 @@ public interface IDatabaseAdapter
/// The parameter to bind to.
/// The value to bind to .
/// is .
- public void BindParameterValue(DbParameter parameter, Object? value);
+ public void BindParameterValue(DbParameter parameter, object? value);
///
/// Returns with the appropriate prefix
@@ -41,7 +41,7 @@ public interface IDatabaseAdapter
/// formatted with the appropriate prefix, suitable for inclusion in SQL
/// statements.
///
- public String FormatParameterName(String parameterName);
+ public string FormatParameterName(string parameterName);
///
/// Gets the corresponding database specific data type for the type .
@@ -67,7 +67,7 @@ public interface IDatabaseAdapter
///
///
///
- public String GetDataType(Type type, EnumSerializationMode enumSerializationMode);
+ public string GetDataType(Type type, EnumSerializationMode enumSerializationMode);
///
/// Returns properly quoted for use in SQL statements.
@@ -76,7 +76,7 @@ public interface IDatabaseAdapter
///
/// A string containing the quoted version of , suitable for use in SQL statements.
///
- public String QuoteIdentifier(String identifier);
+ public string QuoteIdentifier(string identifier);
///
/// Returns the specified name of a temporary table properly quoted for use in SQL statements.
@@ -86,7 +86,7 @@ public interface IDatabaseAdapter
///
/// A string containing the quoted version of , suitable for use in SQL statements.
///
- public String QuoteTemporaryTableName(String tableName, DbConnection connection);
+ public string QuoteTemporaryTableName(string tableName, DbConnection connection);
///
/// Determines whether the database system this adapter supports has support for (local/session scoped) temporary
@@ -107,7 +107,7 @@ public interface IDatabaseAdapter
/// from the property.
///
///
- public Boolean SupportsTemporaryTables(DbConnection connection);
+ public bool SupportsTemporaryTables(DbConnection connection);
///
/// Determines whether was thrown because an SQL statement was cancelled via
@@ -120,8 +120,5 @@ public interface IDatabaseAdapter
/// ; otherwise, .
///
/// is .
- public Boolean WasSqlStatementCancelledByCancellationToken(
- Exception exception,
- CancellationToken cancellationToken
- );
+ public bool WasSqlStatementCancelledByCancellationToken(Exception exception, CancellationToken cancellationToken);
}
diff --git a/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs b/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs
index c542531..b957dbc 100644
--- a/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs
+++ b/src/DbConnectionPlus/DatabaseAdapters/IEntityManipulator.cs
@@ -56,15 +56,12 @@ public interface IEntityManipulator
/// Use or to configure key properties.
///
///
- public Int32 DeleteEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int DeleteEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
CancellationToken cancellationToken
- ) =>
- 0;
+ ) => 0;
///
/// Asynchronously deletes the specified entities, identified by their key property/properties, from the database.
@@ -114,9 +111,7 @@ CancellationToken cancellationToken
/// Use or to configure key properties.
///
///
- public Task DeleteEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public Task DeleteEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -168,15 +163,12 @@ CancellationToken cancellationToken
/// Use or to configure key properties.
///
///
- public Int32 DeleteEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int DeleteEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
CancellationToken cancellationToken
- ) =>
- 0;
+ ) => 0;
///
/// Asynchronously deletes the specified entity, identified by its key property / properties, from the database.
@@ -226,9 +218,7 @@ CancellationToken cancellationToken
/// Use or to configure key properties.
///
///
- public Task DeleteEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public Task DeleteEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -288,15 +278,12 @@ CancellationToken cancellationToken
/// properties are updated accordingly.
///
///
- public Int32 InsertEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int InsertEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
CancellationToken cancellationToken
- ) =>
- 0;
+ ) => 0;
///
/// Asynchronously inserts the specified entities into the database.
@@ -353,9 +340,7 @@ CancellationToken cancellationToken
/// properties are updated accordingly.
///
///
- public Task InsertEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public Task InsertEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -414,15 +399,12 @@ CancellationToken cancellationToken
/// properties are updated accordingly.
///
///
- public Int32 InsertEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int InsertEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
CancellationToken cancellationToken
- ) =>
- 0;
+ ) => 0;
///
/// Asynchronously inserts the specified entity into the database.
@@ -479,9 +461,7 @@ CancellationToken cancellationToken
/// properties are updated accordingly.
///
///
- public Task InsertEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public Task InsertEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
@@ -552,15 +532,12 @@ CancellationToken cancellationToken
/// properties are updated accordingly.
///
///
- public Int32 UpdateEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int UpdateEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
CancellationToken cancellationToken
- ) =>
- 0;
+ ) => 0;
///
/// Asynchronously updates the specified entities, identified by their key property / properties, in the database.
@@ -630,9 +607,7 @@ CancellationToken cancellationToken
/// properties are updated accordingly.
///
///
- public Task UpdateEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public Task UpdateEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction,
@@ -703,15 +678,12 @@ CancellationToken cancellationToken
/// properties are updated accordingly.
///
///
- public Int32 UpdateEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public int UpdateEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
CancellationToken cancellationToken
- ) =>
- 0;
+ ) => 0;
///
/// Asynchronously updates the specified entity, identified by its key property / properties, in the database.
@@ -780,9 +752,7 @@ CancellationToken cancellationToken
/// properties are updated accordingly.
///
///
- public Task UpdateEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public Task UpdateEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbConnection connection,
TEntity entity,
DbTransaction? transaction,
diff --git a/src/DbConnectionPlus/DatabaseAdapters/ITemporaryTableBuilder.cs b/src/DbConnectionPlus/DatabaseAdapters/ITemporaryTableBuilder.cs
index 2b366a4..4ee5c35 100644
--- a/src/DbConnectionPlus/DatabaseAdapters/ITemporaryTableBuilder.cs
+++ b/src/DbConnectionPlus/DatabaseAdapters/ITemporaryTableBuilder.cs
@@ -64,7 +64,7 @@ public interface ITemporaryTableBuilder
///
///
/// If the type is a scalar type
- /// (e.g. , , , and so on),
+ /// (e.g. , , , and so on),
/// a single-column table will be built with a column named "Value" with a data type that matches the
/// type .
///
@@ -80,10 +80,9 @@ public interface ITemporaryTableBuilder
public TemporaryTableDisposer BuildTemporaryTable(
DbConnection connection,
DbTransaction? transaction,
- String name,
+ string name,
IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
CancellationToken cancellationToken = default
);
@@ -145,7 +144,7 @@ public TemporaryTableDisposer BuildTemporaryTable(
///
///
/// If the type is a scalar type
- /// (e.g. , , , and so on),
+ /// (e.g. , , , and so on),
/// a single-column table will be built with a column named "Value" with a data type that matches
/// the type .
///
@@ -161,10 +160,9 @@ public TemporaryTableDisposer BuildTemporaryTable(
public Task BuildTemporaryTableAsync(
DbConnection connection,
DbTransaction? transaction,
- String name,
+ string name,
IEnumerable values,
- [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type valuesType,
+ [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType,
CancellationToken cancellationToken = default
);
}
diff --git a/src/DbConnectionPlus/DatabaseAdapters/TemporaryTableDisposer.cs b/src/DbConnectionPlus/DatabaseAdapters/TemporaryTableDisposer.cs
index e56f1c6..3612e69 100644
--- a/src/DbConnectionPlus/DatabaseAdapters/TemporaryTableDisposer.cs
+++ b/src/DbConnectionPlus/DatabaseAdapters/TemporaryTableDisposer.cs
@@ -9,6 +9,11 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters;
///
public sealed class TemporaryTableDisposer : IDisposable, IAsyncDisposable
{
+ private readonly Func dropTableAsyncFunction;
+ private readonly Action dropTableFunction;
+
+ private bool isDisposed;
+
///
/// Initializes a new instance of the class.
///
@@ -65,9 +70,4 @@ public ValueTask DisposeAsync()
this.isDisposed = true;
return this.dropTableAsyncFunction();
}
-
- private readonly Func dropTableAsyncFunction;
- private readonly Action dropTableFunction;
-
- private Boolean isDisposed;
}
diff --git a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs
index 28d1616..90ae060 100644
--- a/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs
+++ b/src/DbConnectionPlus/DbCommands/DbCommandBuilder.cs
@@ -149,12 +149,13 @@ internal static (DbCommand, DbCommandDisposer) BuildDbCommand(
if (statement.TemporaryTables.Count > 0)
{
temporaryTableDisposers = await BuildTemporaryTablesAsync(
- statement.TemporaryTables,
- databaseAdapter,
- connection,
- transaction,
- cancellationToken
- ).ConfigureAwait(false);
+ statement.TemporaryTables,
+ databaseAdapter,
+ connection,
+ transaction,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
}
return (command, new(command, temporaryTableDisposers, cancellationTokenRegistration));
@@ -185,9 +186,9 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore(
CancellationToken cancellationToken = default
)
{
- using var codeBuilder = new ValueStringBuilder(stackalloc Char[512]);
+ using var codeBuilder = new ValueStringBuilder(stackalloc char[512]);
- var parameterNameOccurrences = new Dictionary(
+ var parameterNameOccurrences = new Dictionary(
statement.Fragments.Count,
StringComparer.OrdinalIgnoreCase
);
@@ -201,7 +202,7 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore(
if (commandTimeout is not null)
{
- command.CommandTimeout = (Int32)commandTimeout.Value.TotalSeconds;
+ command.CommandTimeout = (int)commandTimeout.Value.TotalSeconds;
}
var dbParameters = command.Parameters;
@@ -215,39 +216,38 @@ private static (DbCommand, CancellationTokenRegistration) BuildDbCommandCore(
break;
case InterpolatedParameter interpolatedParameter:
- {
- var parameterName = interpolatedParameter.InferredName ??
- "Parameter_" + (parameterCount + 1);
+ {
+ var parameterName = interpolatedParameter.InferredName ?? "Parameter_" + (parameterCount + 1);
- if (!parameterNameOccurrences.TryAdd(parameterName, 1))
- {
- // Parameter name is already used, so we append a suffix to make it unique.
- var count = ++parameterNameOccurrences[parameterName];
- parameterName += count;
- }
+ if (!parameterNameOccurrences.TryAdd(parameterName, 1))
+ {
+ // Parameter name is already used, so we append a suffix to make it unique.
+ var count = ++parameterNameOccurrences[parameterName];
+ parameterName += count;
+ }
- var dbParameter = command.CreateParameter();
- dbParameter.ParameterName = parameterName;
- databaseAdapter.BindParameterValue(dbParameter, interpolatedParameter.Value);
- dbParameters.Add(dbParameter);
+ var dbParameter = command.CreateParameter();
+ dbParameter.ParameterName = parameterName;
+ databaseAdapter.BindParameterValue(dbParameter, interpolatedParameter.Value);
+ dbParameters.Add(dbParameter);
- codeBuilder.Append(databaseAdapter.FormatParameterName(parameterName));
+ codeBuilder.Append(databaseAdapter.FormatParameterName(parameterName));
- parameterCount++;
- break;
- }
+ parameterCount++;
+ break;
+ }
case Parameter parameter:
- {
- var dbParameter = command.CreateParameter();
- dbParameter.ParameterName = parameter.Name;
- databaseAdapter.BindParameterValue(dbParameter, parameter.Value);
- dbParameters.Add(dbParameter);
-
- parameterNameOccurrences[parameter.Name] = 1;
- parameterCount++;
- break;
- }
+ {
+ var dbParameter = command.CreateParameter();
+ dbParameter.ParameterName = parameter.Name;
+ databaseAdapter.BindParameterValue(dbParameter, parameter.Value);
+ dbParameters.Add(dbParameter);
+
+ parameterNameOccurrences[parameter.Name] = 1;
+ parameterCount++;
+ break;
+ }
case InterpolatedTemporaryTable interpolatedTemporaryTable:
codeBuilder.Append(
@@ -313,9 +313,8 @@ CancellationToken cancellationToken
);
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
foreach (var temporaryTableDisposer in temporaryTableDisposers)
{
@@ -368,19 +367,20 @@ CancellationToken cancellationToken
{
var interpolatedTemporaryTable = temporaryTables[i];
- temporaryTableDisposers[i] = await databaseAdapter.TemporaryTableBuilder.BuildTemporaryTableAsync(
- connection,
- transaction,
- interpolatedTemporaryTable.Name,
- interpolatedTemporaryTable.Values,
- interpolatedTemporaryTable.ValuesType,
- cancellationToken
- ).ConfigureAwait(false);
+ temporaryTableDisposers[i] = await databaseAdapter
+ .TemporaryTableBuilder.BuildTemporaryTableAsync(
+ connection,
+ transaction,
+ interpolatedTemporaryTable.Name,
+ interpolatedTemporaryTable.Values,
+ interpolatedTemporaryTable.ValuesType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
foreach (var temporaryTableDisposer in temporaryTableDisposers)
{
diff --git a/src/DbConnectionPlus/DbCommands/DbCommandDisposer.cs b/src/DbConnectionPlus/DbCommands/DbCommandDisposer.cs
index 976cc9e..aaa802a 100644
--- a/src/DbConnectionPlus/DbCommands/DbCommandDisposer.cs
+++ b/src/DbConnectionPlus/DbCommands/DbCommandDisposer.cs
@@ -10,6 +10,12 @@ namespace RentADeveloper.DbConnectionPlus.DbCommands;
///
internal class DbCommandDisposer : IDisposable, IAsyncDisposable
{
+ private readonly CancellationTokenRegistration cancellationTokenRegistration;
+ private readonly DbCommand command;
+ private readonly TemporaryTableDisposer[] temporaryTableDisposers;
+
+ private bool isDisposed;
+
///
/// Initializes a new instance of the class.
///
@@ -90,10 +96,4 @@ public async ValueTask DisposeAsync()
await tableDisposer.DisposeAsync().ConfigureAwait(false);
}
}
-
- private readonly CancellationTokenRegistration cancellationTokenRegistration;
- private readonly DbCommand command;
- private readonly TemporaryTableDisposer[] temporaryTableDisposers;
-
- private Boolean isDisposed;
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs b/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs
index 34dc38b..cbe1a42 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.Configuration.cs
@@ -1,7 +1,6 @@
// Copyright (c) 2026 David Liebeherr
// Licensed under the MIT License. See LICENSE.md in the project root for more information.
-using RentADeveloper.DbConnectionPlus.Entities;
using RentADeveloper.DbConnectionPlus.SqlStatements;
namespace RentADeveloper.DbConnectionPlus;
@@ -11,6 +10,8 @@ namespace RentADeveloper.DbConnectionPlus;
///
public static partial class DbConnectionExtensions
{
+ private static readonly object configurationLockObject = new();
+
///
/// Configures DbConnectionPlus.
///
@@ -44,8 +45,5 @@ public static void Configure(Action configureActi
internal static void OnBeforeExecutingCommand(
DbCommand command,
IReadOnlyList temporaryTables
- ) =>
- DbConnectionPlusConfiguration.Instance.InterceptDbCommand?.Invoke(command, temporaryTables);
-
- private static readonly Object configurationLockObject = new();
+ ) => DbConnectionPlusConfiguration.Instance.InterceptDbCommand?.Invoke(command, temporaryTables);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs
index 1c116e3..307037a 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntities.cs
@@ -58,20 +58,18 @@ public static partial class DbConnectionExtensions
///
///
/// using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;
- ///
+ ///
/// class Product
/// {
/// [Key]
/// public Int64 Id { get; set; }
/// public Boolean IsDiscontinued { get; set; }
/// }
- ///
+ ///
/// connection.DeleteEntities(products.Where(a => a.IsDiscontinued));
///
///
- public static Int32 DeleteEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public static int DeleteEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
this DbConnection connection,
IEnumerable entities,
DbTransaction? transaction = null,
@@ -84,12 +82,7 @@ public static Int32 DeleteEntities<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- return databaseAdapter.EntityManipulator.DeleteEntities(
- connection,
- entities,
- transaction,
- cancellationToken
- );
+ return databaseAdapter.EntityManipulator.DeleteEntities(connection, entities, transaction, cancellationToken);
}
///
@@ -143,20 +136,18 @@ public static Int32 DeleteEntities<
///
///
/// using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;
- ///
+ ///
/// class Product
/// {
/// [Key]
/// public Int64 Id { get; set; }
/// public Boolean IsDiscontinued { get; set; }
/// }
- ///
+ ///
/// await connection.DeleteEntitiesAsync(products.Where(a => a.IsDiscontinued));
///
///
- public static Task DeleteEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public static Task DeleteEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
this DbConnection connection,
IEnumerable entities,
DbTransaction? transaction = null,
@@ -169,7 +160,11 @@ public static Task DeleteEntitiesAsync<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- return databaseAdapter.EntityManipulator
- .DeleteEntitiesAsync(connection, entities, transaction, cancellationToken);
+ return databaseAdapter.EntityManipulator.DeleteEntitiesAsync(
+ connection,
+ entities,
+ transaction,
+ cancellationToken
+ );
}
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs
index f4036a0..b8c0221 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.DeleteEntity.cs
@@ -58,23 +58,21 @@ public static partial class DbConnectionExtensions
///
///
/// using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;
- ///
+ ///
/// class Product
/// {
/// [Key]
/// public Int64 Id { get; set; }
/// public Boolean IsDiscontinued { get; set; }
/// }
- ///
+ ///
/// if (product.IsDiscontinued)
/// {
/// connection.DeleteEntity(product);
/// }
///
///
- public static Int32 DeleteEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public static int DeleteEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
this DbConnection connection,
TEntity entity,
DbTransaction? transaction = null,
@@ -87,12 +85,7 @@ public static Int32 DeleteEntity<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- return databaseAdapter.EntityManipulator.DeleteEntity(
- connection,
- entity,
- transaction,
- cancellationToken
- );
+ return databaseAdapter.EntityManipulator.DeleteEntity(connection, entity, transaction, cancellationToken);
}
///
@@ -146,23 +139,21 @@ public static Int32 DeleteEntity<
///
///
/// using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;
- ///
+ ///
/// class Product
/// {
/// [Key]
/// public Int64 Id { get; set; }
/// public Boolean IsDiscontinued { get; set; }
/// }
- ///
+ ///
/// if (product.IsDiscontinued)
/// {
/// await connection.DeleteEntityAsync(product);
/// }
///
///
- public static Task DeleteEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public static Task DeleteEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
this DbConnection connection,
TEntity entity,
DbTransaction? transaction = null,
@@ -175,11 +166,6 @@ public static Task DeleteEntityAsync<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- return databaseAdapter.EntityManipulator.DeleteEntityAsync(
- connection,
- entity,
- transaction,
- cancellationToken
- );
+ return databaseAdapter.EntityManipulator.DeleteEntityAsync(connection, entity, transaction, cancellationToken);
}
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteNonQuery.cs b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteNonQuery.cs
index 9b99813..8d585f0 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteNonQuery.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteNonQuery.cs
@@ -31,7 +31,7 @@ public static partial class DbConnectionExtensions
///
///
/// using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;
- ///
+ ///
/// if (supplier.IsRetired)
/// {
/// var numberOfDeletedProducts = connection.ExecuteNonQuery(
@@ -40,7 +40,7 @@ public static partial class DbConnectionExtensions
/// }
///
///
- public static Int32 ExecuteNonQuery(
+ public static int ExecuteNonQuery(
this DbConnection connection,
InterpolatedSqlStatement statement,
DbTransaction? transaction = null,
@@ -70,9 +70,8 @@ public static Int32 ExecuteNonQuery(
OnBeforeExecutingCommand(command, statement.TemporaryTables);
return command.ExecuteNonQuery();
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -103,7 +102,7 @@ public static Int32 ExecuteNonQuery(
///
///
/// using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;
- ///
+ ///
/// if (supplier.IsRetired)
/// {
/// var numberOfDeletedProducts = await connection.ExecuteNonQueryAsync(
@@ -112,7 +111,7 @@ public static Int32 ExecuteNonQuery(
/// }
///
///
- public static async Task ExecuteNonQueryAsync(
+ public static async Task ExecuteNonQueryAsync(
this DbConnection connection,
InterpolatedSqlStatement statement,
DbTransaction? transaction = null,
@@ -125,15 +124,17 @@ public static async Task ExecuteNonQueryAsync(
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
await using (commandDisposer)
{
@@ -142,9 +143,8 @@ public static async Task ExecuteNonQueryAsync(
OnBeforeExecutingCommand(command, statement.TemporaryTables);
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs
index 62489eb..cbdaa88 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteReader.cs
@@ -39,9 +39,9 @@ public static partial class DbConnectionExtensions
///
///
/// ExecuteReaderAsync(
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
DbDataReader? dataReader = null;
@@ -175,9 +176,8 @@ public static async Task ExecuteReaderAsync(
cancellationToken
);
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
if (dataReader is not null)
{
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteScalar.cs b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteScalar.cs
index 7a76954..8b9b347 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.ExecuteScalar.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.ExecuteScalar.cs
@@ -47,9 +47,9 @@ public static partial class DbConnectionExtensions
///
/// (
/// $"SELECT COUNT(*) FROM Product WHERE UnitsInStock < {Parameter(lowStockThreshold)}"
/// );
@@ -90,12 +90,11 @@ public static TTarget ExecuteScalar(
{
null => default!, // If the result set is empty, we get null and must return default of TTarget.
TTarget alreadyTargetTypeValue => alreadyTargetTypeValue,
- _ => ConvertValueForExecuteScalar(value)
+ _ => ConvertValueForExecuteScalar(value),
};
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -139,9 +138,9 @@ public static TTarget ExecuteScalar(
///
/// (
/// $"SELECT COUNT(*) FROM Product WHERE UnitsInStock < {Parameter(lowStockThreshold)}"
/// );
@@ -161,15 +160,17 @@ public static async Task ExecuteScalarAsync(
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
await using (commandDisposer)
{
@@ -182,12 +183,11 @@ public static async Task ExecuteScalarAsync(
{
null => default!, // If the result set is empty, we get null and must return default of TTarget.
TTarget alreadyTargetTypeValue => alreadyTargetTypeValue,
- _ => ConvertValueForExecuteScalar(value)
+ _ => ConvertValueForExecuteScalar(value),
};
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -205,7 +205,7 @@ public static async Task ExecuteScalarAsync(
///
/// could not be converted to the type .
///
- private static TTarget ConvertValueForExecuteScalar(Object? value)
+ private static TTarget ConvertValueForExecuteScalar(object? value)
{
try
{
@@ -214,17 +214,17 @@ private static TTarget ConvertValueForExecuteScalar(Object? value)
catch (Exception exception) when (value is null or DBNull)
{
throw new InvalidCastException(
- "The first column of the first row in the result set returned by the SQL statement contains a NULL " +
- $"value, which could not be converted to the type {typeof(TTarget)}. See inner exception for details.",
+ "The first column of the first row in the result set returned by the SQL statement contains a NULL "
+ + $"value, which could not be converted to the type {typeof(TTarget)}. See inner exception for details.",
exception
);
}
catch (Exception exception) when (value is not null)
{
throw new InvalidCastException(
- "The first column of the first row in the result set returned by the SQL statement contains " +
- $"the value {value.ToDebugString()}, which could not be converted to the type {typeof(TTarget)}. " +
- "See inner exception for details.",
+ "The first column of the first row in the result set returned by the SQL statement contains "
+ + $"the value {value.ToDebugString()}, which could not be converted to the type {typeof(TTarget)}. "
+ + "See inner exception for details.",
exception
);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Exists.cs b/src/DbConnectionPlus/DbConnectionExtensions.Exists.cs
index 7b729d8..8df8fee 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.Exists.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.Exists.cs
@@ -12,7 +12,7 @@ namespace RentADeveloper.DbConnectionPlus;
public static partial class DbConnectionExtensions
{
///
- /// Executes the specified SQL statement and returns a value indicating whether the result
+ /// Executes the specified SQL statement and returns a value indicating whether the result
/// set returned by the statement contains at least one row.
/// This method is intended to check for the existence of rows matching certain criteria, e.g. checking whether a
/// Product with a specific Id exists.
@@ -38,16 +38,16 @@ public static partial class DbConnectionExtensions
///
///
///
///
- public static Boolean Exists(
+ public static bool Exists(
this DbConnection connection,
InterpolatedSqlStatement statement,
DbTransaction? transaction = null,
@@ -78,9 +78,8 @@ public static Boolean Exists(
using var reader = command.ExecuteReader(CommandBehavior.SingleResult | CommandBehavior.SingleRow);
return reader.Read();
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -88,7 +87,7 @@ public static Boolean Exists(
}
///
- /// Asynchronously executes the specified SQL statement and returns a value indicating
+ /// Asynchronously executes the specified SQL statement and returns a value indicating
/// whether the result set returned by the statement contains at least one row.
/// This method is intended to check for the existence of rows matching certain criteria, e.g. checking whether a
/// Product with a specific Id exists.
@@ -116,16 +115,16 @@ public static Boolean Exists(
///
///
///
///
- public static async Task ExistsAsync(
+ public static async Task ExistsAsync(
this DbConnection connection,
InterpolatedSqlStatement statement,
DbTransaction? transaction = null,
@@ -138,15 +137,17 @@ public static async Task ExistsAsync(
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
await using (commandDisposer)
{
@@ -154,17 +155,15 @@ public static async Task ExistsAsync(
{
OnBeforeExecutingCommand(command, statement.TemporaryTables);
#pragma warning disable CA2007
- await using var reader = await command.ExecuteReaderAsync(
- CommandBehavior.SingleResult | CommandBehavior.SingleRow,
- cancellationToken
- ).ConfigureAwait(false);
+ await using var reader = await command
+ .ExecuteReaderAsync(CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancellationToken)
+ .ConfigureAwait(false);
#pragma warning restore CA2007
return await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs
index 128ba03..c8dbe77 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntities.cs
@@ -65,7 +65,7 @@ public static partial class DbConnectionExtensions
///
///
/// using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;
- ///
+ ///
/// class Product
/// {
/// public Int64 Id { get; set; }
@@ -74,15 +74,13 @@ public static partial class DbConnectionExtensions
/// public Decimal UnitPrice { get; set; }
/// public Int32 UnitsInStock { get; set; }
/// }
- ///
+ ///
/// var newProducts = GetNewProducts();
- ///
+ ///
/// connection.InsertEntities(newProducts);
///
///
- public static Int32 InsertEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public static int InsertEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
this DbConnection connection,
IEnumerable entities,
DbTransaction? transaction = null,
@@ -95,12 +93,7 @@ public static Int32 InsertEntities<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- return databaseAdapter.EntityManipulator.InsertEntities(
- connection,
- entities,
- transaction,
- cancellationToken
- );
+ return databaseAdapter.EntityManipulator.InsertEntities(connection, entities, transaction, cancellationToken);
}
///
@@ -161,7 +154,7 @@ public static Int32 InsertEntities<
///
///
/// using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;
- ///
+ ///
/// class Product
/// {
/// public Int64 Id { get; set; }
@@ -170,15 +163,13 @@ public static Int32 InsertEntities<
/// public Decimal UnitPrice { get; set; }
/// public Int32 UnitsInStock { get; set; }
/// }
- ///
+ ///
/// var newProducts = await GetNewProductsAsync();
- ///
+ ///
/// await connection.InsertEntitiesAsync(newProducts);
///
///
- public static Task InsertEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public static Task InsertEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
this DbConnection connection,
IEnumerable entities,
DbTransaction? transaction = null,
@@ -191,12 +182,11 @@ public static Task InsertEntitiesAsync<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- return databaseAdapter.EntityManipulator
- .InsertEntitiesAsync(
- connection,
- entities,
- transaction,
- cancellationToken
- );
+ return databaseAdapter.EntityManipulator.InsertEntitiesAsync(
+ connection,
+ entities,
+ transaction,
+ cancellationToken
+ );
}
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs
index 0c5a95a..011285d 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.InsertEntity.cs
@@ -65,7 +65,7 @@ public static partial class DbConnectionExtensions
///
///
/// using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;
- ///
+ ///
/// class Product
/// {
/// public Int64 Id { get; set; }
@@ -74,15 +74,13 @@ public static partial class DbConnectionExtensions
/// public Decimal UnitPrice { get; set; }
/// public Int32 UnitsInStock { get; set; }
/// }
- ///
+ ///
/// var newProduct = GetNewProduct();
- ///
+ ///
/// connection.InsertEntity(newProduct);
///
///
- public static Int32 InsertEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public static int InsertEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
this DbConnection connection,
TEntity entity,
DbTransaction? transaction = null,
@@ -95,12 +93,7 @@ public static Int32 InsertEntity<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- return databaseAdapter.EntityManipulator.InsertEntity(
- connection,
- entity,
- transaction,
- cancellationToken
- );
+ return databaseAdapter.EntityManipulator.InsertEntity(connection, entity, transaction, cancellationToken);
}
///
@@ -161,7 +154,7 @@ public static Int32 InsertEntity<
///
///
/// using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;
- ///
+ ///
/// class Product
/// {
/// public Int64 Id { get; set; }
@@ -170,15 +163,13 @@ public static Int32 InsertEntity<
/// public Decimal UnitPrice { get; set; }
/// public Int32 UnitsInStock { get; set; }
/// }
- ///
+ ///
/// var newProduct = await GetNewProductAsync();
- ///
+ ///
/// await connection.InsertEntityAsync(newProduct);
///
///
- public static Task InsertEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public static Task InsertEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
this DbConnection connection,
TEntity entity,
DbTransaction? transaction = null,
@@ -191,11 +182,6 @@ public static Task InsertEntityAsync<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- return databaseAdapter.EntityManipulator.InsertEntityAsync(
- connection,
- entity,
- transaction,
- cancellationToken
- );
+ return databaseAdapter.EntityManipulator.InsertEntityAsync(connection, entity, transaction, cancellationToken);
}
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs b/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs
index 57d9dd7..0a9ba20 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.Parameter.cs
@@ -11,6 +11,11 @@ namespace RentADeveloper.DbConnectionPlus;
///
public static partial class DbConnectionExtensions
{
+ ///
+ /// The maximum length for inferred parameter names. This length is supported by all major database systems.
+ ///
+ private const int MaximumParameterNameLength = 60;
+
///
///
/// Wraps in an instance of to indicate
@@ -38,9 +43,9 @@ public static partial class DbConnectionExtensions
///
///
///
public static InterpolatedParameter Parameter(
- Object? parameterValue,
- [CallerArgumentExpression(nameof(parameterValue))]
- String? parameterValueExpression = null
+ object? parameterValue,
+ [CallerArgumentExpression(nameof(parameterValue))] string? parameterValueExpression = null
)
{
- String? inferredParameterName = null;
+ string? inferredParameterName = null;
if (parameterValueExpression?.Length > 0)
{
@@ -88,9 +92,4 @@ public static InterpolatedParameter Parameter(
return new(inferredParameterName, parameterValue);
}
-
- ///
- /// The maximum length for inferred parameter names. This length is supported by all major database systems.
- ///
- private const Int32 MaximumParameterNameLength = 60;
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.Query.cs b/src/DbConnectionPlus/DbConnectionExtensions.Query.cs
index 45d986f..4321f1a 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.Query.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.Query.cs
@@ -3,8 +3,8 @@
using RentADeveloper.DbConnectionPlus.Materializers;
using RentADeveloper.DbConnectionPlus.SqlStatements;
-using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder;
using DataRow = RentADeveloper.DbConnectionPlus.Dynamic.DataRow;
+using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder;
namespace RentADeveloper.DbConnectionPlus;
@@ -102,9 +102,8 @@ public static IEnumerable Query(
OnBeforeExecutingCommand(command, statement.TemporaryTables);
reader = command.ExecuteReader(CommandBehavior.SequentialAccess);
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
reader?.Dispose();
@@ -126,9 +125,8 @@ public static IEnumerable Query(
yield break;
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -211,15 +209,17 @@ public static async IAsyncEnumerable QueryAsync(
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
await using (commandDisposer)
{
@@ -228,12 +228,12 @@ public static async IAsyncEnumerable QueryAsync(
try
{
OnBeforeExecutingCommand(command, statement.TemporaryTables);
- reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)
+ reader = await command
+ .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)
.ConfigureAwait(false);
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
if (reader is not null)
{
@@ -256,9 +256,8 @@ public static async IAsyncEnumerable QueryAsync(
yield break;
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs
index 8f61c54..4f28566 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirst.cs
@@ -3,8 +3,8 @@
using RentADeveloper.DbConnectionPlus.Materializers;
using RentADeveloper.DbConnectionPlus.SqlStatements;
-using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder;
using DataRow = RentADeveloper.DbConnectionPlus.Dynamic.DataRow;
+using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder;
namespace RentADeveloper.DbConnectionPlus;
@@ -105,9 +105,8 @@ public static DataRow QueryFirst(
return DataRowMaterializer.Materialize(reader);
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -181,25 +180,25 @@ public static async Task QueryFirstAsync(
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
using (commandDisposer)
{
try
{
OnBeforeExecutingCommand(command, statement.TemporaryTables);
- var reader = await command.ExecuteReaderAsync(
- CommandBehavior.SingleResult | CommandBehavior.SingleRow,
- cancellationToken
- )
+ var reader = await command
+ .ExecuteReaderAsync(CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancellationToken)
.ConfigureAwait(false);
await using (reader)
@@ -212,9 +211,8 @@ public static async Task QueryFirstAsync(
return DataRowMaterializer.Materialize(reader);
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs
index bac7c9c..4a68e69 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOfT.cs
@@ -95,7 +95,7 @@ public static partial class DbConnectionExtensions
///
///
/// A built-in .NET type or a nullable built-in .NET type like or
- /// .
+ /// .
///
///
/// In this case only the first column of the result set will be read and converted to the type
@@ -109,33 +109,33 @@ public static partial class DbConnectionExtensions
/// In this case the first row in the result set will be materialized into an instance of the entity
/// type, with the constructor arguments or properties of the entity being populated from the
/// corresponding columns of the row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
- ///
+ ///
/// The type must either:
- ///
+ ///
/// 1. Have a constructor whose parameters match the columns of the result set returned by the
/// statement.
/// The names of the parameters must match the names of the columns (case-insensitive).
/// The types of the parameters must be compatible with the data types of the columns.
/// The compatibility is determined using .
/// The parameters can be in any order.
- ///
+ ///
/// Or
- ///
+ ///
/// 2. Have a parameterless constructor and properties (with public setters) that match the columns of
/// the result set returned by the statement.
- ///
+ ///
/// Per default, the names of the properties must match the names of the columns (case-insensitive).
/// This can be configured via or .
- ///
+ ///
/// The types of the properties must be compatible with the data types of the columns.
/// The compatibility is determined using .
- ///
+ ///
/// Columns without a matching property will be ignored.
- ///
+ ///
/// If neither condition is satisfied, an will be thrown.
- ///
+ ///
/// If a constructor parameter or a property cannot be set to the value of the corresponding column
/// due to a type mismatch, an will be thrown.
///
@@ -146,14 +146,14 @@ public static partial class DbConnectionExtensions
/// In this case the first row in the result set will be materialized into an instance of the value
/// tuple type, with the fields of the value tuple being populated from the corresponding columns of the
/// row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
/// The SQL statement must return the same number of columns as the value tuple has fields.
/// The SQL statement must return the columns in the same order as the fields in the value tuple.
- ///
+ ///
/// The data types of the columns must be compatible with the field types of the value tuple.
/// The compatibility is determined using .
- ///
+ ///
/// If those conditions are not met, an is thrown.
///
///
@@ -164,7 +164,7 @@ public static partial class DbConnectionExtensions
///
/// ($"SELECT * FROM [Order] WHERE Id = {Parameter(orderId)}");
/// ]]>
///
///
- public static T QueryFirst<
- [DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T
- >(
+ public static T QueryFirst<[DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T>(
this DbConnection connection,
InterpolatedSqlStatement statement,
DbTransaction? transaction = null,
@@ -211,8 +209,8 @@ public static T QueryFirst<
using (reader)
{
- var isTBuiltInTypeOrEnumType = typeof(T).IsBuiltInTypeOrNullableBuiltInType() ||
- typeof(T).IsEnumOrNullableEnumType();
+ var isTBuiltInTypeOrEnumType =
+ typeof(T).IsBuiltInTypeOrNullableBuiltInType() || typeof(T).IsEnumOrNullableEnumType();
var isTValueTupleType = typeof(T).IsValueTupleType();
var isTEntityType = !isTBuiltInTypeOrEnumType && !isTValueTupleType;
@@ -253,9 +251,8 @@ public static T QueryFirst<
return entityMaterializer(reader);
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -344,7 +341,7 @@ public static T QueryFirst<
///
///
/// A built-in .NET type or a nullable built-in .NET type like or
- /// .
+ /// .
///
///
/// In this case only the first column of the result set will be read and converted to the type
@@ -358,33 +355,33 @@ public static T QueryFirst<
/// In this case the first row in the result set will be materialized into an instance of the entity
/// type, with the constructor arguments or properties of the entity being populated from the
/// corresponding columns of the row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
- ///
+ ///
/// The type must either:
- ///
+ ///
/// 1. Have a constructor whose parameters match the columns of the result set returned by the
/// statement.
/// The names of the parameters must match the names of the columns (case-insensitive).
/// The types of the parameters must be compatible with the data types of the columns.
/// The compatibility is determined using .
/// The parameters can be in any order.
- ///
+ ///
/// Or
- ///
+ ///
/// 2. Have a parameterless constructor and properties (with public setters) that match the columns of
/// the result set returned by the statement.
- ///
+ ///
/// Per default, the names of the properties must match the names of the columns (case-insensitive).
/// This can be configured via or .
- ///
+ ///
/// The types of the properties must be compatible with the data types of the columns.
/// The compatibility is determined using .
- ///
+ ///
/// Columns without a matching property will be ignored.
- ///
+ ///
/// If neither condition is satisfied, an will be thrown.
- ///
+ ///
/// If a constructor parameter or a property cannot be set to the value of the corresponding column
/// due to a type mismatch, an will be thrown.
///
@@ -395,14 +392,14 @@ public static T QueryFirst<
/// In this case the first row in the result set will be materialized into an instance of the value
/// tuple type, with the fields of the value tuple being populated from the corresponding columns of the
/// row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
/// The SQL statement must return the same number of columns as the value tuple has fields.
/// The SQL statement must return the columns in the same order as the fields in the value tuple.
- ///
+ ///
/// The data types of the columns must be compatible with the field types of the value tuple.
/// The compatibility is determined using .
- ///
+ ///
/// If those conditions are not met, an is thrown.
///
///
@@ -413,7 +410,7 @@ public static T QueryFirst<
///
/// ($"SELECT * FROM [Order] WHERE Id = {Parameter(orderId)}");
/// ]]>
///
///
- public static async Task QueryFirstAsync<
- [DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T
- >(
+ public static async Task QueryFirstAsync<[DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T>(
this DbConnection connection,
InterpolatedSqlStatement statement,
DbTransaction? transaction = null,
@@ -441,31 +436,31 @@ public static async Task QueryFirstAsync<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
using (commandDisposer)
{
try
{
OnBeforeExecutingCommand(command, statement.TemporaryTables);
- var reader = await command.ExecuteReaderAsync(
- CommandBehavior.SingleResult | CommandBehavior.SingleRow,
- cancellationToken
- )
+ var reader = await command
+ .ExecuteReaderAsync(CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancellationToken)
.ConfigureAwait(false);
await using (reader)
{
- var isTBuiltInTypeOrEnumType = typeof(T).IsBuiltInTypeOrNullableBuiltInType() ||
- typeof(T).IsEnumOrNullableEnumType();
+ var isTBuiltInTypeOrEnumType =
+ typeof(T).IsBuiltInTypeOrNullableBuiltInType() || typeof(T).IsEnumOrNullableEnumType();
var isTValueTupleType = typeof(T).IsValueTupleType();
var isTEntityType = !isTBuiltInTypeOrEnumType && !isTValueTupleType;
@@ -506,9 +501,8 @@ public static async Task QueryFirstAsync<
return entityMaterializer(reader);
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefault.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefault.cs
index f043c50..61f761d 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefault.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefault.cs
@@ -3,8 +3,8 @@
using RentADeveloper.DbConnectionPlus.Materializers;
using RentADeveloper.DbConnectionPlus.SqlStatements;
-using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder;
using DataRow = RentADeveloper.DbConnectionPlus.Dynamic.DataRow;
+using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder;
namespace RentADeveloper.DbConnectionPlus;
@@ -111,9 +111,8 @@ public static partial class DbConnectionExtensions
return DataRowMaterializer.Materialize(reader);
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -193,25 +192,25 @@ public static partial class DbConnectionExtensions
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
using (commandDisposer)
{
try
{
OnBeforeExecutingCommand(command, statement.TemporaryTables);
- var reader = await command.ExecuteReaderAsync(
- CommandBehavior.SingleResult | CommandBehavior.SingleRow,
- cancellationToken
- )
+ var reader = await command
+ .ExecuteReaderAsync(CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancellationToken)
.ConfigureAwait(false);
await using (reader)
@@ -224,9 +223,8 @@ public static partial class DbConnectionExtensions
return DataRowMaterializer.Materialize(reader);
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs
index f34f58d..46d94d2 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryFirstOrDefaultOfT.cs
@@ -95,7 +95,7 @@ public static partial class DbConnectionExtensions
///
///
/// A built-in .NET type or a nullable built-in .NET type like or
- /// .
+ /// .
///
///
/// In this case only the first column of the result set will be read and converted to the type
@@ -109,33 +109,33 @@ public static partial class DbConnectionExtensions
/// In this case the first row in the result set will be materialized into an instance of the entity
/// type, with the constructor arguments or properties of the entity being populated from the
/// corresponding columns of the row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
- ///
+ ///
/// The type must either:
- ///
+ ///
/// 1. Have a constructor whose parameters match the columns of the result set returned by the
/// statement.
/// The names of the parameters must match the names of the columns (case-insensitive).
/// The types of the parameters must be compatible with the data types of the columns.
/// The compatibility is determined using .
/// The parameters can be in any order.
- ///
+ ///
/// Or
- ///
+ ///
/// 2. Have a parameterless constructor and properties (with public setters) that match the columns of
/// the result set returned by the statement.
- ///
+ ///
/// Per default, the names of the properties must match the names of the columns (case-insensitive).
/// This can be configured via or .
- ///
+ ///
/// The types of the properties must be compatible with the data types of the columns.
/// The compatibility is determined using .
- ///
+ ///
/// Columns without a matching property will be ignored.
- ///
+ ///
/// If neither condition is satisfied, an will be thrown.
- ///
+ ///
/// If a constructor parameter or a property cannot be set to the value of the corresponding column
/// due to a type mismatch, an will be thrown.
///
@@ -146,14 +146,14 @@ public static partial class DbConnectionExtensions
/// In this case the first row in the result set will be materialized into an instance of the value
/// tuple type, with the fields of the value tuple being populated from the corresponding columns of the
/// row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
/// The SQL statement must return the same number of columns as the value tuple has fields.
/// The SQL statement must return the columns in the same order as the fields in the value tuple.
- ///
+ ///
/// The data types of the columns must be compatible with the field types of the value tuple.
/// The compatibility is determined using .
- ///
+ ///
/// If those conditions are not met, an is thrown.
///
///
@@ -164,7 +164,7 @@ public static partial class DbConnectionExtensions
///
/// ($"SELECT * FROM [Order] WHERE Id = {Parameter(orderId)}");
/// ]]>
///
///
- public static T? QueryFirstOrDefault<
- [DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T
- >(
+ public static T? QueryFirstOrDefault<[DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T>(
this DbConnection connection,
InterpolatedSqlStatement statement,
DbTransaction? transaction = null,
@@ -211,8 +209,8 @@ public static T? QueryFirstOrDefault<
using (reader)
{
- var isTBuiltInTypeOrEnumType = typeof(T).IsBuiltInTypeOrNullableBuiltInType() ||
- typeof(T).IsEnumOrNullableEnumType();
+ var isTBuiltInTypeOrEnumType =
+ typeof(T).IsBuiltInTypeOrNullableBuiltInType() || typeof(T).IsEnumOrNullableEnumType();
var isTValueTupleType = typeof(T).IsValueTupleType();
var isTEntityType = !isTBuiltInTypeOrEnumType && !isTValueTupleType;
@@ -253,9 +251,8 @@ public static T? QueryFirstOrDefault<
return entityMaterializer(reader);
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -346,7 +343,7 @@ public static T? QueryFirstOrDefault<
///
///
/// A built-in .NET type or a nullable built-in .NET type like or
- /// .
+ /// .
///
///
/// In this case only the first column of the result set will be read and converted to the type
@@ -360,33 +357,33 @@ public static T? QueryFirstOrDefault<
/// In this case the first row in the result set will be materialized into an instance of the entity
/// type, with the constructor arguments or properties of the entity being populated from the
/// corresponding columns of the row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
- ///
+ ///
/// The type must either:
- ///
+ ///
/// 1. Have a constructor whose parameters match the columns of the result set returned by the
/// statement.
/// The names of the parameters must match the names of the columns (case-insensitive).
/// The types of the parameters must be compatible with the data types of the columns.
/// The compatibility is determined using .
/// The parameters can be in any order.
- ///
+ ///
/// Or
- ///
+ ///
/// 2. Have a parameterless constructor and properties (with public setters) that match the columns of
/// the result set returned by the statement.
- ///
+ ///
/// Per default, the names of the properties must match the names of the columns (case-insensitive).
/// This can be configured via or .
- ///
+ ///
/// The types of the properties must be compatible with the data types of the columns.
/// The compatibility is determined using .
- ///
+ ///
/// Columns without a matching property will be ignored.
- ///
+ ///
/// If neither condition is satisfied, an will be thrown.
- ///
+ ///
/// If a constructor parameter or a property cannot be set to the value of the corresponding column
/// due to a type mismatch, an will be thrown.
///
@@ -397,14 +394,14 @@ public static T? QueryFirstOrDefault<
/// In this case the first row in the result set will be materialized into an instance of the value
/// tuple type, with the fields of the value tuple being populated from the corresponding columns of the
/// row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
/// The SQL statement must return the same number of columns as the value tuple has fields.
/// The SQL statement must return the columns in the same order as the fields in the value tuple.
- ///
+ ///
/// The data types of the columns must be compatible with the field types of the value tuple.
/// The compatibility is determined using .
- ///
+ ///
/// If those conditions are not met, an is thrown.
///
///
@@ -415,7 +412,7 @@ public static T? QueryFirstOrDefault<
///
/// (
/// $"SELECT * FROM [Order] WHERE Id = {Parameter(orderId)}"
/// );
@@ -445,31 +442,31 @@ public static async Task QueryFirstOrDefaultAsync<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
using (commandDisposer)
{
try
{
OnBeforeExecutingCommand(command, statement.TemporaryTables);
- var reader = await command.ExecuteReaderAsync(
- CommandBehavior.SingleResult | CommandBehavior.SingleRow,
- cancellationToken
- )
+ var reader = await command
+ .ExecuteReaderAsync(CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancellationToken)
.ConfigureAwait(false);
await using (reader)
{
- var isTBuiltInTypeOrEnumType = typeof(T).IsBuiltInTypeOrNullableBuiltInType() ||
- typeof(T).IsEnumOrNullableEnumType();
+ var isTBuiltInTypeOrEnumType =
+ typeof(T).IsBuiltInTypeOrNullableBuiltInType() || typeof(T).IsEnumOrNullableEnumType();
var isTValueTupleType = typeof(T).IsValueTupleType();
var isTEntityType = !isTBuiltInTypeOrEnumType && !isTValueTupleType;
@@ -510,9 +507,8 @@ public static async Task QueryFirstOrDefaultAsync<
return entityMaterializer(reader);
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QueryOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QueryOfT.cs
index 03d04c7..1aa1ee0 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.QueryOfT.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.QueryOfT.cs
@@ -92,7 +92,7 @@ public static partial class DbConnectionExtensions
///
///
/// A built-in .NET type or a nullable built-in .NET type like or
- /// .
+ /// .
///
///
/// In this case only the first column of the result set will be read and converted to the type
@@ -106,33 +106,33 @@ public static partial class DbConnectionExtensions
/// In this case each row in the result set will be materialized into an instance of the entity type,
/// with the constructor arguments or properties of the entity being populated from the corresponding
/// columns of the row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
- ///
+ ///
/// The type must either:
- ///
+ ///
/// 1. Have a constructor whose parameters match the columns of the result set returned by the
/// statement.
/// The names of the parameters must match the names of the columns (case-insensitive).
/// The types of the parameters must be compatible with the data types of the columns.
/// The compatibility is determined using .
/// The parameters can be in any order.
- ///
+ ///
/// Or
- ///
+ ///
/// 2. Have a parameterless constructor and properties (with public setters) that match the columns of
/// the result set returned by the statement.
- ///
+ ///
/// Per default, the names of the properties must match the names of the columns (case-insensitive).
/// This can be configured via or .
- ///
+ ///
/// The types of the properties must be compatible with the data types of the columns.
/// The compatibility is determined using .
- ///
+ ///
/// Columns without a matching property will be ignored.
- ///
+ ///
/// If neither condition is satisfied, an will be thrown.
- ///
+ ///
/// If a constructor parameter or a property cannot be set to the value of the corresponding column
/// due to a type mismatch, an will be thrown.
///
@@ -143,14 +143,14 @@ public static partial class DbConnectionExtensions
/// In this case each row in the result set will be materialized into an instance of the value tuple
/// type, with the fields of the value tuple being populated from the corresponding columns of the
/// row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
/// The SQL statement must return the same number of columns as the value tuple has fields.
/// The SQL statement must return the columns in the same order as the fields in the value tuple.
- ///
+ ///
/// The data types of the columns must be compatible with the field types of the value tuple.
/// The compatibility is determined using .
- ///
+ ///
/// If those conditions are not met, an is thrown.
///
///
@@ -161,18 +161,16 @@ public static partial class DbConnectionExtensions
///
/// (
/// $"SELECT * FROM Product WHERE UnitsInStock < {Parameter(lowStockThreshold)}"
/// );
/// ]]>
///
///
- public static IEnumerable Query<
- [DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T
- >(
+ public static IEnumerable Query<[DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T>(
this DbConnection connection,
InterpolatedSqlStatement statement,
DbTransaction? transaction = null,
@@ -204,9 +202,8 @@ public static IEnumerable Query<
OnBeforeExecutingCommand(command, statement.TemporaryTables);
reader = command.ExecuteReader();
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
reader?.Dispose();
@@ -215,8 +212,8 @@ public static IEnumerable Query<
using (reader)
{
- var isTBuiltInTypeOrEnumType = typeof(T).IsBuiltInTypeOrNullableBuiltInType() ||
- typeof(T).IsEnumOrNullableEnumType();
+ var isTBuiltInTypeOrEnumType =
+ typeof(T).IsBuiltInTypeOrNullableBuiltInType() || typeof(T).IsEnumOrNullableEnumType();
var isTValueTupleType = typeof(T).IsValueTupleType();
var isTEntityType = !isTBuiltInTypeOrEnumType && !isTValueTupleType;
@@ -245,9 +242,8 @@ public static IEnumerable Query<
yield break;
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -356,7 +352,7 @@ public static IEnumerable Query<
///
///
/// A built-in .NET type or a nullable built-in .NET type like or
- /// .
+ /// .
///
///
/// In this case only the first column of the result set will be read and converted to the type
@@ -370,33 +366,33 @@ public static IEnumerable Query<
/// In this case each row in the result set will be materialized into an instance of the entity type,
/// with the constructor arguments or properties of the entity being populated from the corresponding
/// columns of the row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
- ///
+ ///
/// The type must either:
- ///
+ ///
/// 1. Have a constructor whose parameters match the columns of the result set returned by the
/// statement.
/// The names of the parameters must match the names of the columns (case-insensitive).
/// The types of the parameters must be compatible with the data types of the columns.
/// The compatibility is determined using .
/// The parameters can be in any order.
- ///
+ ///
/// Or
- ///
+ ///
/// 2. Have a parameterless constructor and properties (with public setters) that match the columns of
/// the result set returned by the statement.
- ///
+ ///
/// Per default, the names of the properties must match the names of the columns (case-insensitive).
/// This can be configured via or .
- ///
+ ///
/// The types of the properties must be compatible with the data types of the columns.
/// The compatibility is determined using .
- ///
+ ///
/// Columns without a matching property will be ignored.
- ///
+ ///
/// If neither condition is satisfied, an will be thrown.
- ///
+ ///
/// If a constructor parameter or a property cannot be set to the value of the corresponding column
/// due to a type mismatch, an will be thrown.
///
@@ -407,14 +403,14 @@ public static IEnumerable Query<
/// In this case each row in the result set will be materialized into an instance of the value tuple
/// type, with the fields of the value tuple being populated from the corresponding columns of the
/// row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
/// The SQL statement must return the same number of columns as the value tuple has fields.
/// The SQL statement must return the columns in the same order as the fields in the value tuple.
- ///
+ ///
/// The data types of the columns must be compatible with the field types of the value tuple.
/// The compatibility is determined using .
- ///
+ ///
/// If those conditions are not met, an is thrown.
///
///
@@ -425,9 +421,9 @@ public static IEnumerable Query<
///
/// (
/// $"SELECT * FROM Product WHERE UnitsInStock < {Parameter(lowStockThreshold)}"
/// );
@@ -449,15 +445,17 @@ public static async IAsyncEnumerable QueryAsync<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
using (commandDisposer)
{
@@ -468,9 +466,8 @@ public static async IAsyncEnumerable QueryAsync<
OnBeforeExecutingCommand(command, statement.TemporaryTables);
reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
if (reader is not null)
{
@@ -482,8 +479,8 @@ public static async IAsyncEnumerable QueryAsync<
await using (reader)
{
- var isTBuiltInTypeOrEnumType = typeof(T).IsBuiltInTypeOrNullableBuiltInType() ||
- typeof(T).IsEnumOrNullableEnumType();
+ var isTBuiltInTypeOrEnumType =
+ typeof(T).IsBuiltInTypeOrNullableBuiltInType() || typeof(T).IsEnumOrNullableEnumType();
var isTValueTupleType = typeof(T).IsValueTupleType();
var isTEntityType = !isTBuiltInTypeOrEnumType && !isTValueTupleType;
@@ -512,9 +509,8 @@ public static async IAsyncEnumerable QueryAsync<
yield break;
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -555,7 +551,7 @@ public static async IAsyncEnumerable QueryAsync<
///
/// could not be converted to the type .
///
- private static TTarget ConvertValueForQuery(Object? value)
+ private static TTarget ConvertValueForQuery(object? value)
{
try
{
@@ -564,16 +560,16 @@ private static TTarget ConvertValueForQuery(Object? value)
catch (Exception exception) when (value is DBNull)
{
throw new InvalidCastException(
- "The first column returned by the SQL statement contains a NULL value, which could not be converted " +
- $"to the type {typeof(TTarget)}. See inner exception for details.",
+ "The first column returned by the SQL statement contains a NULL value, which could not be converted "
+ + $"to the type {typeof(TTarget)}. See inner exception for details.",
exception
);
}
catch (Exception exception) when (value is not null)
{
throw new InvalidCastException(
- $"The first column returned by the SQL statement contains the value {value.ToDebugString()}, which " +
- $"could not be converted to the type {typeof(TTarget)}. See inner exception for details.",
+ $"The first column returned by the SQL statement contains the value {value.ToDebugString()}, which "
+ + $"could not be converted to the type {typeof(TTarget)}. See inner exception for details.",
exception
);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingle.cs b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingle.cs
index 5dd7e0e..6dc13b0 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingle.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingle.cs
@@ -3,8 +3,8 @@
using RentADeveloper.DbConnectionPlus.Materializers;
using RentADeveloper.DbConnectionPlus.SqlStatements;
-using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder;
using DataRow = RentADeveloper.DbConnectionPlus.Dynamic.DataRow;
+using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder;
namespace RentADeveloper.DbConnectionPlus;
@@ -125,9 +125,8 @@ public static DataRow QuerySingle(
return dataRow;
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -214,25 +213,25 @@ public static async Task QuerySingleAsync(
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
using (commandDisposer)
{
try
{
OnBeforeExecutingCommand(command, statement.TemporaryTables);
- var reader = await command.ExecuteReaderAsync(
- CommandBehavior.SingleResult,
- cancellationToken
- )
+ var reader = await command
+ .ExecuteReaderAsync(CommandBehavior.SingleResult, cancellationToken)
.ConfigureAwait(false);
await using (reader)
@@ -252,9 +251,8 @@ public static async Task QuerySingleAsync(
return dataRow;
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOfT.cs
index 8da0827..6dde24b 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOfT.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOfT.cs
@@ -108,7 +108,7 @@ public static partial class DbConnectionExtensions
///
///
/// A built-in .NET type or a nullable built-in .NET type like or
- /// .
+ /// .
///
///
/// In this case only the first column of the result set will be read and converted to the type
@@ -122,33 +122,33 @@ public static partial class DbConnectionExtensions
/// In this case the single row in the result set will be materialized into an instance of the entity
/// type, with the constructor arguments or properties of the entity being populated from the
/// corresponding columns of the row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
- ///
+ ///
/// The type must either:
- ///
+ ///
/// 1. Have a constructor whose parameters match the columns of the result set returned by the
/// statement.
/// The names of the parameters must match the names of the columns (case-insensitive).
/// The types of the parameters must be compatible with the data types of the columns.
/// The compatibility is determined using .
/// The parameters can be in any order.
- ///
+ ///
/// Or
- ///
+ ///
/// 2. Have a parameterless constructor and properties (with public setters) that match the columns of
/// the result set returned by the statement.
- ///
+ ///
/// Per default, the names of the properties must match the names of the columns (case-insensitive).
/// This can be configured via or .
- ///
+ ///
/// The types of the properties must be compatible with the data types of the columns.
/// The compatibility is determined using .
- ///
+ ///
/// Columns without a matching property will be ignored.
- ///
+ ///
/// If neither condition is satisfied, an will be thrown.
- ///
+ ///
/// If a constructor parameter or a property cannot be set to the value of the corresponding column
/// due to a type mismatch, an will be thrown.
///
@@ -159,14 +159,14 @@ public static partial class DbConnectionExtensions
/// In this case the single row in the result set will be materialized into an instance of the value
/// tuple type, with the fields of the value tuple being populated from the corresponding columns of the
/// row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
/// The SQL statement must return the same number of columns as the value tuple has fields.
/// The SQL statement must return the columns in the same order as the fields in the value tuple.
- ///
+ ///
/// The data types of the columns must be compatible with the field types of the value tuple.
/// The compatibility is determined using .
- ///
+ ///
/// If those conditions are not met, an is thrown.
///
///
@@ -177,7 +177,7 @@ public static partial class DbConnectionExtensions
///
/// ($"SELECT * FROM [Order] WHERE Id = {Parameter(orderId)}");
/// ]]>
///
///
- public static T QuerySingle<
- [DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T
- >(
+ public static T QuerySingle<[DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T>(
this DbConnection connection,
InterpolatedSqlStatement statement,
DbTransaction? transaction = null,
@@ -224,8 +222,8 @@ public static T QuerySingle<
using (reader)
{
- var isTBuiltInTypeOrEnumType = typeof(T).IsBuiltInTypeOrNullableBuiltInType() ||
- typeof(T).IsEnumOrNullableEnumType();
+ var isTBuiltInTypeOrEnumType =
+ typeof(T).IsBuiltInTypeOrNullableBuiltInType() || typeof(T).IsEnumOrNullableEnumType();
var isTValueTupleType = typeof(T).IsValueTupleType();
var isTEntityType = !isTBuiltInTypeOrEnumType && !isTValueTupleType;
@@ -276,9 +274,8 @@ public static T QuerySingle<
return result;
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -380,7 +377,7 @@ public static T QuerySingle<
///
///
/// A built-in .NET type or a nullable built-in .NET type like or
- /// .
+ /// .
///
///
/// In this case only the first column of the result set will be read and converted to the type
@@ -394,33 +391,33 @@ public static T QuerySingle<
/// In this case the single row in the result set will be materialized into an instance of the entity
/// type, with the constructor arguments or properties of the entity being populated from the
/// corresponding columns of the row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
- ///
+ ///
/// The type must either:
- ///
+ ///
/// 1. Have a constructor whose parameters match the columns of the result set returned by the
/// statement.
/// The names of the parameters must match the names of the columns (case-insensitive).
/// The types of the parameters must be compatible with the data types of the columns.
/// The compatibility is determined using .
/// The parameters can be in any order.
- ///
+ ///
/// Or
- ///
+ ///
/// 2. Have a parameterless constructor and properties (with public setters) that match the columns of
/// the result set returned by the statement.
- ///
+ ///
/// Per default, the names of the properties must match the names of the columns (case-insensitive).
/// This can be configured via or .
- ///
+ ///
/// The types of the properties must be compatible with the data types of the columns.
/// The compatibility is determined using .
- ///
+ ///
/// Columns without a matching property will be ignored.
- ///
+ ///
/// If neither condition is satisfied, an will be thrown.
- ///
+ ///
/// If a constructor parameter or a property cannot be set to the value of the corresponding column
/// due to a type mismatch, an will be thrown.
///
@@ -431,14 +428,14 @@ public static T QuerySingle<
/// In this case the single row in the result set will be materialized into an instance of the value
/// tuple type, with the fields of the value tuple being populated from the corresponding columns of the
/// row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
/// The SQL statement must return the same number of columns as the value tuple has fields.
/// The SQL statement must return the columns in the same order as the fields in the value tuple.
- ///
+ ///
/// The data types of the columns must be compatible with the field types of the value tuple.
/// The compatibility is determined using .
- ///
+ ///
/// If those conditions are not met, an is thrown.
///
///
@@ -449,7 +446,7 @@ public static T QuerySingle<
///
/// ($"SELECT * FROM [Order] WHERE Id = {Parameter(orderId)}");
/// ]]>
///
///
- public static async Task QuerySingleAsync<
- [DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T
- >(
+ public static async Task QuerySingleAsync<[DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T>(
this DbConnection connection,
InterpolatedSqlStatement statement,
DbTransaction? transaction = null,
@@ -477,28 +472,31 @@ public static async Task QuerySingleAsync<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
using (commandDisposer)
{
try
{
OnBeforeExecutingCommand(command, statement.TemporaryTables);
- var reader = await command.ExecuteReaderAsync(CommandBehavior.SingleResult, cancellationToken)
+ var reader = await command
+ .ExecuteReaderAsync(CommandBehavior.SingleResult, cancellationToken)
.ConfigureAwait(false);
await using (reader)
{
- var isTBuiltInTypeOrEnumType = typeof(T).IsBuiltInTypeOrNullableBuiltInType() ||
- typeof(T).IsEnumOrNullableEnumType();
+ var isTBuiltInTypeOrEnumType =
+ typeof(T).IsBuiltInTypeOrNullableBuiltInType() || typeof(T).IsEnumOrNullableEnumType();
var isTValueTupleType = typeof(T).IsValueTupleType();
var isTEntityType = !isTBuiltInTypeOrEnumType && !isTValueTupleType;
@@ -549,9 +547,8 @@ public static async Task QuerySingleAsync<
return result;
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefault.cs b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefault.cs
index 41c8cd7..61ce0e4 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefault.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefault.cs
@@ -3,8 +3,8 @@
using RentADeveloper.DbConnectionPlus.Materializers;
using RentADeveloper.DbConnectionPlus.SqlStatements;
-using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder;
using DataRow = RentADeveloper.DbConnectionPlus.Dynamic.DataRow;
+using DbCommandBuilder = RentADeveloper.DbConnectionPlus.DbCommands.DbCommandBuilder;
namespace RentADeveloper.DbConnectionPlus;
@@ -119,9 +119,8 @@ public static partial class DbConnectionExtensions
return dataRow;
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -202,25 +201,25 @@ public static partial class DbConnectionExtensions
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
using (commandDisposer)
{
try
{
OnBeforeExecutingCommand(command, statement.TemporaryTables);
- var reader = await command.ExecuteReaderAsync(
- CommandBehavior.SingleResult,
- cancellationToken
- )
+ var reader = await command
+ .ExecuteReaderAsync(CommandBehavior.SingleResult, cancellationToken)
.ConfigureAwait(false);
await using (reader)
@@ -240,9 +239,8 @@ public static partial class DbConnectionExtensions
return dataRow;
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefaultOfT.cs b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefaultOfT.cs
index d708320..8644d2b 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefaultOfT.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.QuerySingleOrDefaultOfT.cs
@@ -96,7 +96,7 @@ public static partial class DbConnectionExtensions
///
///
/// A built-in .NET type or a nullable built-in .NET type like or
- /// .
+ /// .
///
///
/// In this case only the first column of the result set will be read and converted to the type
@@ -110,33 +110,33 @@ public static partial class DbConnectionExtensions
/// In this case the single row in the result set will be materialized into an instance of the entity
/// type, with the constructor arguments or properties of the entity being populated from the
/// corresponding columns of the row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
- ///
+ ///
/// The type must either:
- ///
+ ///
/// 1. Have a constructor whose parameters match the columns of the result set returned by the
/// statement.
/// The names of the parameters must match the names of the columns (case-insensitive).
/// The types of the parameters must be compatible with the data types of the columns.
/// The compatibility is determined using .
/// The parameters can be in any order.
- ///
+ ///
/// Or
- ///
+ ///
/// 2. Have a parameterless constructor and properties (with public setters) that match the columns of
/// the result set returned by the statement.
- ///
+ ///
/// Per default, the names of the properties must match the names of the columns (case-insensitive).
/// This can be configured via or .
- ///
+ ///
/// The types of the properties must be compatible with the data types of the columns.
/// The compatibility is determined using .
- ///
+ ///
/// Columns without a matching property will be ignored.
- ///
+ ///
/// If neither condition is satisfied, an will be thrown.
- ///
+ ///
/// If a constructor parameter or a property cannot be set to the value of the corresponding column
/// due to a type mismatch, an will be thrown.
///
@@ -147,14 +147,14 @@ public static partial class DbConnectionExtensions
/// In this case the single row in the result set will be materialized into an instance of the value
/// tuple type, with the fields of the value tuple being populated from the corresponding columns of the
/// row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
/// The SQL statement must return the same number of columns as the value tuple has fields.
/// The SQL statement must return the columns in the same order as the fields in the value tuple.
- ///
+ ///
/// The data types of the columns must be compatible with the field types of the value tuple.
/// The compatibility is determined using .
- ///
+ ///
/// If those conditions are not met, an is thrown.
///
///
@@ -165,7 +165,7 @@ public static partial class DbConnectionExtensions
///
/// ($"SELECT * FROM [Order] WHERE Id = {Parameter(orderId)}");
/// ]]>
///
///
- public static T? QuerySingleOrDefault<
- [DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T
- >(
+ public static T? QuerySingleOrDefault<[DynamicallyAccessedMembers(EntityHelper.QueryResultMemberTypes)] T>(
this DbConnection connection,
InterpolatedSqlStatement statement,
DbTransaction? transaction = null,
@@ -212,8 +210,8 @@ public static T? QuerySingleOrDefault<
using (reader)
{
- var isTBuiltInTypeOrEnumType = typeof(T).IsBuiltInTypeOrNullableBuiltInType() ||
- typeof(T).IsEnumOrNullableEnumType();
+ var isTBuiltInTypeOrEnumType =
+ typeof(T).IsBuiltInTypeOrNullableBuiltInType() || typeof(T).IsEnumOrNullableEnumType();
var isTValueTupleType = typeof(T).IsValueTupleType();
var isTEntityType = !isTBuiltInTypeOrEnumType && !isTValueTupleType;
@@ -262,9 +260,8 @@ public static T? QuerySingleOrDefault<
return result;
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
@@ -355,7 +352,7 @@ public static T? QuerySingleOrDefault<
///
///
/// A built-in .NET type or a nullable built-in .NET type like or
- /// .
+ /// .
///
///
/// In this case only the first column of the result set will be read and converted to the type
@@ -369,33 +366,33 @@ public static T? QuerySingleOrDefault<
/// In this case the single row in the result set will be materialized into an instance of the entity
/// type, with the constructor arguments or properties of the entity being populated from the
/// corresponding columns of the row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
- ///
+ ///
/// The type must either:
- ///
+ ///
/// 1. Have a constructor whose parameters match the columns of the result set returned by the
/// statement.
/// The names of the parameters must match the names of the columns (case-insensitive).
/// The types of the parameters must be compatible with the data types of the columns.
/// The compatibility is determined using .
/// The parameters can be in any order.
- ///
+ ///
/// Or
- ///
+ ///
/// 2. Have a parameterless constructor and properties (with public setters) that match the columns of
/// the result set returned by the statement.
- ///
+ ///
/// Per default, the names of the properties must match the names of the columns (case-insensitive).
/// This can be configured via or .
- ///
+ ///
/// The types of the properties must be compatible with the data types of the columns.
/// The compatibility is determined using .
- ///
+ ///
/// Columns without a matching property will be ignored.
- ///
+ ///
/// If neither condition is satisfied, an will be thrown.
- ///
+ ///
/// If a constructor parameter or a property cannot be set to the value of the corresponding column
/// due to a type mismatch, an will be thrown.
///
@@ -406,14 +403,14 @@ public static T? QuerySingleOrDefault<
/// In this case the single row in the result set will be materialized into an instance of the value
/// tuple type, with the fields of the value tuple being populated from the corresponding columns of the
/// row.
- ///
+ ///
/// All columns returned by the SQL statement must have a name.
/// The SQL statement must return the same number of columns as the value tuple has fields.
/// The SQL statement must return the columns in the same order as the fields in the value tuple.
- ///
+ ///
/// The data types of the columns must be compatible with the field types of the value tuple.
/// The compatibility is determined using .
- ///
+ ///
/// If those conditions are not met, an is thrown.
///
///
@@ -424,7 +421,7 @@ public static T? QuerySingleOrDefault<
///
/// (
/// $"SELECT * FROM [Order] WHERE Id = {Parameter(orderId)}"
/// );
@@ -454,28 +451,31 @@ public static async Task QuerySingleOrDefaultAsync<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- var (command, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync(
- statement,
- databaseAdapter,
- connection,
- transaction,
- commandTimeout,
- commandType,
- cancellationToken
- ).ConfigureAwait(false);
+ var (command, commandDisposer) = await DbCommandBuilder
+ .BuildDbCommandAsync(
+ statement,
+ databaseAdapter,
+ connection,
+ transaction,
+ commandTimeout,
+ commandType,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
using (commandDisposer)
{
try
{
OnBeforeExecutingCommand(command, statement.TemporaryTables);
- var reader = await command.ExecuteReaderAsync(CommandBehavior.SingleResult, cancellationToken)
+ var reader = await command
+ .ExecuteReaderAsync(CommandBehavior.SingleResult, cancellationToken)
.ConfigureAwait(false);
await using (reader)
{
- var isTBuiltInTypeOrEnumType = typeof(T).IsBuiltInTypeOrNullableBuiltInType() ||
- typeof(T).IsEnumOrNullableEnumType();
+ var isTBuiltInTypeOrEnumType =
+ typeof(T).IsBuiltInTypeOrNullableBuiltInType() || typeof(T).IsEnumOrNullableEnumType();
var isTValueTupleType = typeof(T).IsValueTupleType();
var isTEntityType = !isTBuiltInTypeOrEnumType && !isTValueTupleType;
@@ -524,9 +524,8 @@ public static async Task QuerySingleOrDefaultAsync<
return result;
}
}
- catch (Exception exception) when (
- databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)
- )
+ catch (Exception exception)
+ when (databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.TemporaryTable.cs b/src/DbConnectionPlus/DbConnectionExtensions.TemporaryTable.cs
index e040a6c..76bc56d 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.TemporaryTable.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.TemporaryTable.cs
@@ -32,7 +32,7 @@ public static partial class DbConnectionExtensions
/// An instance of indicating that the sequence
/// should be passed as a temporary table to an SQL statement.
///
- /// is the type .
+ /// is the type .
/// is .
///
/// To use this method import with a using directive with the static modifier:
@@ -40,7 +40,7 @@ public static partial class DbConnectionExtensions
/// using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;
///
///
- /// You can pass a sequence of scalar values (e.g. , ,
+ /// You can pass a sequence of scalar values (e.g. , ,
/// , and so on) or a sequence of complex objects.
///
///
@@ -51,9 +51,9 @@ public static partial class DbConnectionExtensions
///
/// a.IsRetired).Select(a => a.Id);
- ///
+ ///
/// var retiredSupplierProductsReader = connection.ExecuteReader(
/// $"""
/// SELECT *
@@ -87,16 +87,16 @@ public static partial class DbConnectionExtensions
///
///
public static InterpolatedTemporaryTable TemporaryTable<
[DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] T
- >(
- IEnumerable values,
- [CallerArgumentExpression(nameof(values))]
- String? valuesExpression = null
- )
+ >(IEnumerable values, [CallerArgumentExpression(nameof(values))] string? valuesExpression = null)
{
ArgumentNullException.ThrowIfNull(values);
- if (typeof(T) == typeof(Object))
+ if (typeof(T) == typeof(object))
{
- throw new ArgumentException($"The type parameter T cannot be the type {typeof(Object)}.");
+ throw new ArgumentException($"The type parameter T cannot be the type {typeof(object)}.");
}
- String? temporaryTableName = null;
+ string? temporaryTableName = null;
- if (!String.IsNullOrWhiteSpace(valuesExpression))
+ if (!string.IsNullOrWhiteSpace(valuesExpression))
{
var nameFromCallerArgumentExpression = NameHelper.CreateNameFromCallerArgumentExpression(
valuesExpression,
@@ -158,13 +154,13 @@ public static InterpolatedTemporaryTable TemporaryTable<
27
);
- if (!String.IsNullOrWhiteSpace(nameFromCallerArgumentExpression))
+ if (!string.IsNullOrWhiteSpace(nameFromCallerArgumentExpression))
{
temporaryTableName = nameFromCallerArgumentExpression + "_" + Guid.NewGuid().ToString("N");
}
}
- if (String.IsNullOrWhiteSpace(temporaryTableName))
+ if (string.IsNullOrWhiteSpace(temporaryTableName))
{
temporaryTableName = "Values_" + Guid.NewGuid().ToString("N");
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs
index 99c811f..4b4126e 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntities.cs
@@ -79,7 +79,7 @@ public static partial class DbConnectionExtensions
///
/// (
/// """
/// SELECT *
@@ -95,19 +95,17 @@ public static partial class DbConnectionExtensions
/// WHERE LastLoginDate < DATEADD(YEAR, -1, GETUTCDATE())
/// """
/// );
- ///
+ ///
/// foreach (var user in usersWithoutLoginInPastYear)
/// {
/// user.State = UserState.Inactive;
/// }
- ///
+ ///
/// connection.UpdateEntities(usersWithoutLoginInPastYear);
/// ]]>
///
///
- public static Int32 UpdateEntities<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public static int UpdateEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
this DbConnection connection,
IEnumerable entities,
DbTransaction? transaction = null,
@@ -120,12 +118,7 @@ public static Int32 UpdateEntities<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- return databaseAdapter.EntityManipulator.UpdateEntities(
- connection,
- entities,
- transaction,
- cancellationToken
- );
+ return databaseAdapter.EntityManipulator.UpdateEntities(connection, entities, transaction, cancellationToken);
}
///
@@ -200,7 +193,7 @@ public static Int32 UpdateEntities<
///
/// (
/// """
/// SELECT *
@@ -216,19 +209,17 @@ public static Int32 UpdateEntities<
/// WHERE LastLoginDate < DATEADD(YEAR, -1, GETUTCDATE())
/// """
/// );
- ///
+ ///
/// await foreach (var user in usersWithoutLoginInPastYear)
/// {
/// user.State = UserState.Inactive;
/// }
- ///
+ ///
/// await connection.UpdateEntitiesAsync(usersWithoutLoginInPastYear);
/// ]]>
///
///
- public static Task UpdateEntitiesAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public static Task UpdateEntitiesAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
this DbConnection connection,
IEnumerable entities,
DbTransaction? transaction = null,
@@ -241,7 +232,11 @@ public static Task UpdateEntitiesAsync<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- return databaseAdapter.EntityManipulator
- .UpdateEntitiesAsync(connection, entities, transaction, cancellationToken);
+ return databaseAdapter.EntityManipulator.UpdateEntitiesAsync(
+ connection,
+ entities,
+ transaction,
+ cancellationToken
+ );
}
}
diff --git a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs
index 1b85937..938ee9f 100644
--- a/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs
+++ b/src/DbConnectionPlus/DbConnectionExtensions.UpdateEntity.cs
@@ -79,7 +79,7 @@ public static partial class DbConnectionExtensions
///
///
///
///
- public static Int32 UpdateEntity<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public static int UpdateEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
this DbConnection connection,
TEntity entity,
DbTransaction? transaction = null,
@@ -111,12 +109,7 @@ public static Int32 UpdateEntity<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- return databaseAdapter.EntityManipulator.UpdateEntity(
- connection,
- entity,
- transaction,
- cancellationToken
- );
+ return databaseAdapter.EntityManipulator.UpdateEntity(connection, entity, transaction, cancellationToken);
}
///
@@ -190,7 +183,7 @@ public static Int32 UpdateEntity<
///
///
///
///
- public static Task UpdateEntityAsync<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ public static Task UpdateEntityAsync<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
this DbConnection connection,
TEntity entity,
DbTransaction? transaction = null,
@@ -222,11 +213,6 @@ public static Task UpdateEntityAsync<
var databaseAdapter = DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(connection.GetType());
- return databaseAdapter.EntityManipulator.UpdateEntityAsync(
- connection,
- entity,
- transaction,
- cancellationToken
- );
+ return databaseAdapter.EntityManipulator.UpdateEntityAsync(connection, entity, transaction, cancellationToken);
}
}
diff --git a/src/DbConnectionPlus/Dynamic/DataRow.cs b/src/DbConnectionPlus/Dynamic/DataRow.cs
index bb203b5..d127e83 100644
--- a/src/DbConnectionPlus/Dynamic/DataRow.cs
+++ b/src/DbConnectionPlus/Dynamic/DataRow.cs
@@ -39,78 +39,84 @@ namespace RentADeveloper.DbConnectionPlus.Dynamic;
/// var name = product.Name;
///
///
+///
+/// The columns of the data row.
+/// The keys are expected to be the column names, and the values are expected to be the corresponding column values.
+///
#pragma warning disable CA1710
-public class DataRow : IDictionary, IDynamicMetaObjectProvider
+public class DataRow(IDictionary columns) : IDictionary, IDynamicMetaObjectProvider
#pragma warning restore CA1710
{
///
- /// Initializes a new instance of the class.
+ /// Reads the value of a column, used as the target of a bound dynamic member read.
+ ///
+ private static readonly Func readColumn = static (row, columnName) => row[columnName];
+
+ ///
+ /// Writes the value of a column and returns it, used as the target of a bound dynamic member write.
+ ///
+ private static readonly Func writeColumn = static (row, columnName, value) =>
+ row[columnName] = value;
+
+ ///
+ /// The columns of the data row, keyed by column name.
///
- ///
- /// The columns of the data row.
- /// The keys are expected to be the column names, and the values are expected to be the corresponding column values.
- ///
- public DataRow(IDictionary columns) =>
- this.columns = columns;
+ private readonly IDictionary columns = columns;
+
+ ///
+ public int Count => this.columns.Count;
///
- public Int32 Count => this.columns.Count;
+ public bool IsReadOnly => this.columns.IsReadOnly;
///
- public Boolean IsReadOnly => this.columns.IsReadOnly;
+ public ICollection Keys => this.columns.Keys;
///
- public Object? this[String key]
+ public ICollection Values => this.columns.Values;
+
+ ///
+ public object? this[string key]
{
get => this.columns[key];
set => this.columns[key] = value;
}
///
- public ICollection Keys => this.columns.Keys;
+ IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
///
- public ICollection Values => this.columns.Values;
+ DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter) => this.GetMetaObject(parameter);
///
- public void Add(KeyValuePair item) =>
- this.columns.Add(item);
+ public void Add(KeyValuePair item) => this.columns.Add(item);
///
- public void Add(String key, Object? value) =>
- this.columns.Add(key, value);
+ public void Add(string key, object? value) => this.columns.Add(key, value);
///
- public void Clear() =>
- this.columns.Clear();
+ public void Clear() => this.columns.Clear();
///
- public Boolean Contains(KeyValuePair item) =>
- this.columns.Contains(item);
+ public bool Contains(KeyValuePair item) => this.columns.Contains(item);
///
- public Boolean ContainsKey(String key) =>
- this.columns.ContainsKey(key);
+ public bool ContainsKey(string key) => this.columns.ContainsKey(key);
///
- public void CopyTo(KeyValuePair[] array, Int32 arrayIndex) =>
- this.columns.CopyTo(array, arrayIndex);
+ public void CopyTo(KeyValuePair[] array, int arrayIndex) => this.columns.CopyTo(array, arrayIndex);
///
- public IEnumerator> GetEnumerator() =>
- this.columns.GetEnumerator();
+ public IEnumerator> GetEnumerator() => this.columns.GetEnumerator();
///
- public Boolean Remove(KeyValuePair item) =>
- this.columns.Remove(item);
+ public bool Remove(KeyValuePair item) => this.columns.Remove(item);
///
- public Boolean Remove(String key) =>
- this.columns.Remove(key);
+ public bool Remove(string key) => this.columns.Remove(key);
///
- public Boolean TryGetValue(String key, out Object? value) =>
- this.columns.TryGetValue(key, out value);
+ public bool TryGetValue(string key, out object? value) => this.columns.TryGetValue(key, out value);
///
/// Returns the that binds member access on this row to its columns.
@@ -122,30 +128,7 @@ public Boolean TryGetValue(String key, out Object? value) =>
/// reference. It is not meant to be called directly. Override it to change how member
/// access on a derived row is bound.
///
- protected virtual DynamicMetaObject GetMetaObject(Expression parameter) =>
- new DataRowMetaObject(parameter, this);
-
- ///
- DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter) =>
- this.GetMetaObject(parameter);
-
- ///
- IEnumerator IEnumerable.GetEnumerator() =>
- this.GetEnumerator();
-
- ///
- /// Reads the value of a column, used as the target of a bound dynamic member read.
- ///
- private static readonly Func readColumn =
- static (row, columnName) => row[columnName];
-
- ///
- /// Writes the value of a column and returns it, used as the target of a bound dynamic member write.
- ///
- private static readonly Func writeColumn =
- static (row, columnName, value) => row[columnName] = value;
-
- private readonly IDictionary columns;
+ protected virtual DynamicMetaObject GetMetaObject(Expression parameter) => new DataRowMetaObject(parameter, this);
///
/// Binds member access on a to the columns of the row, so that row.Id resolves to
@@ -175,9 +158,7 @@ private sealed class DataRowMetaObject : DynamicMetaObject
/// The expression representing the at the call site.
/// The the member access is bound against.
internal DataRowMetaObject(Expression expression, DataRow row)
- : base(expression, BindingRestrictions.Empty, row)
- {
- }
+ : base(expression, BindingRestrictions.Empty, row) { }
///
public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
@@ -215,22 +196,20 @@ public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicM
Expression.Constant(writeColumn),
this.GetRowExpression(),
Expression.Constant(binder.Name),
- Expression.Convert(value.Expression, typeof(Object))
+ Expression.Convert(value.Expression, typeof(object))
),
this.GetTypeRestriction().Merge(value.Restrictions)
);
}
///
- public override IEnumerable GetDynamicMemberNames() =>
- ((DataRow)this.Value!).Keys;
+ public override IEnumerable GetDynamicMemberNames() => ((DataRow)this.Value!).Keys;
///
/// Gets the call-site expression converted to .
///
/// The call-site expression converted to .
- private UnaryExpression GetRowExpression() =>
- Expression.Convert(this.Expression, typeof(DataRow));
+ private UnaryExpression GetRowExpression() => Expression.Convert(this.Expression, typeof(DataRow));
///
/// Gets the binding restriction that limits the bound call site to the runtime type of the row.
diff --git a/src/DbConnectionPlus/Entities/EntityHelper.cs b/src/DbConnectionPlus/Entities/EntityHelper.cs
index 1e5dbd3..9af213e 100644
--- a/src/DbConnectionPlus/Entities/EntityHelper.cs
+++ b/src/DbConnectionPlus/Entities/EntityHelper.cs
@@ -25,9 +25,9 @@ public static class EntityHelper
/// silently binds fewer columns.
///
internal const DynamicallyAccessedMemberTypes EntityMemberTypes =
- DynamicallyAccessedMemberTypes.PublicConstructors |
- DynamicallyAccessedMemberTypes.NonPublicConstructors |
- DynamicallyAccessedMemberTypes.PublicProperties;
+ DynamicallyAccessedMemberTypes.PublicConstructors
+ | DynamicallyAccessedMemberTypes.NonPublicConstructors
+ | DynamicallyAccessedMemberTypes.PublicProperties;
///
/// The members that must survive trimming for a type used as the result type of a query.
@@ -38,8 +38,7 @@ public static class EntityHelper
/// materializers reflect over — plus the value tuple's public fields.
///
internal const DynamicallyAccessedMemberTypes QueryResultMemberTypes =
- EntityMemberTypes |
- ValueTupleMaterializerFactory.ValueTupleMemberTypes;
+ EntityMemberTypes | ValueTupleMaterializerFactory.ValueTupleMemberTypes;
///
/// The members that must survive trimming for a type whose values are written to a temporary table.
@@ -51,8 +50,9 @@ public static class EntityHelper
/// , whose contract requires the type's public fields and properties.
///
internal const DynamicallyAccessedMemberTypes TemporaryTableValueMemberTypes =
- EntityMemberTypes |
- DynamicallyAccessedMemberTypes.PublicFields;
+ EntityMemberTypes | DynamicallyAccessedMemberTypes.PublicFields;
+
+ private static readonly ConcurrentDictionary entityTypeMetadataPerEntityType = [];
///
/// Tries to find a constructor of the type that has parameters compatible to the
@@ -62,7 +62,7 @@ public static class EntityHelper
/// The type of which to find the constructor.
///
/// The expected parameters of the constructor to find.
- ///
+ ///
/// The constructor to find must have parameters with the same names (case-insensitive) and compatible types.
/// A parameter type is considered compatible if a value of the expected parameter type can be converted to
/// the actual parameter type.
@@ -85,13 +85,13 @@ public static class EntityHelper
///
public static ConstructorInfo? FindCompatibleConstructor(
[DynamicallyAccessedMembers(EntityMemberTypes)] Type type,
- (String Name, Type Type)[] expectedParameters)
+ (string Name, Type Type)[] expectedParameters
+ )
{
ArgumentNullException.ThrowIfNull(type);
ArgumentNullException.ThrowIfNull(expectedParameters);
- var constructors = type
- .GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
+ var constructors = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.OrderByDescending(c => c.IsPublic)
.ThenBy(c => c.IsPrivate)
.ThenBy(c => c.GetParameters().Length);
@@ -105,15 +105,13 @@ public static class EntityHelper
continue;
}
- var areParametersCompatible =
- expectedParameters
- .All(expectedParameter =>
- parameters.Any(parameter =>
- !String.IsNullOrWhiteSpace(parameter.Name) &&
- parameter.Name.Equals(expectedParameter.Name, StringComparison.OrdinalIgnoreCase) &&
- ValueConverter.CanConvert(expectedParameter.Type, parameter.ParameterType)
- )
- );
+ var areParametersCompatible = expectedParameters.All(expectedParameter =>
+ parameters.Any(parameter =>
+ !string.IsNullOrWhiteSpace(parameter.Name)
+ && parameter.Name.Equals(expectedParameter.Name, StringComparison.OrdinalIgnoreCase)
+ && ValueConverter.CanConvert(expectedParameter.Type, parameter.ParameterType)
+ )
+ );
if (areParametersCompatible)
{
@@ -135,12 +133,12 @@ public static class EntityHelper
/// The type of which to find the parameterless constructor.
/// is .
public static ConstructorInfo? FindParameterlessConstructor(
- [DynamicallyAccessedMembers(EntityMemberTypes)] Type type)
+ [DynamicallyAccessedMembers(EntityMemberTypes)] Type type
+ )
{
ArgumentNullException.ThrowIfNull(type);
- return type
- .GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
+ return type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.OrderByDescending(c => c.IsPublic)
.ThenBy(c => c.IsPrivate)
.FirstOrDefault(c => c.GetParameters().Length == 0);
@@ -161,7 +159,8 @@ public static class EntityHelper
/// There is more than one identity property defined for the entity type .
///
public static EntityTypeMetadata GetEntityTypeMetadata(
- [DynamicallyAccessedMembers(EntityMemberTypes)] Type entityType)
+ [DynamicallyAccessedMembers(EntityMemberTypes)] Type entityType
+ )
{
ArgumentNullException.ThrowIfNull(entityType);
@@ -180,54 +179,7 @@ public static EntityTypeMetadata GetEntityTypeMetadata(
///
/// Resets the cached entity types metadata.
///
- internal static void ResetEntityTypeMetadataCache() =>
- entityTypeMetadataPerEntityType.Clear();
-
- ///
- /// Creates the getter function for the property .
- ///
- /// The property for which to create the getter function.
- /// A function taking an entity and returning the value of .
- ///
- /// The underlying is resolved on the first call and then kept in the closure, so
- /// building the metadata of an entity type costs nothing per property until an accessor is actually used. The
- /// unsynchronized assignment is deliberate: two threads racing here produce two equivalent invokers, and either
- /// one is correct.
- ///
- private static Func CreatePropertyGetter(PropertyInfo property)
- {
- MethodInvoker? getMethodInvoker = null;
-
- return entity =>
- {
- getMethodInvoker ??= MethodInvoker.Create(property.GetMethod!);
-
- return getMethodInvoker.Invoke(entity);
- };
- }
-
- ///
- /// Creates the setter function for the property .
- ///
- /// The property for which to create the setter function.
- /// An action taking an entity and the value to assign to .
- ///
- /// The underlying is resolved on the first call and then kept in the closure, so
- /// building the metadata of an entity type costs nothing per property until an accessor is actually used. The
- /// unsynchronized assignment is deliberate: two threads racing here produce two equivalent invokers, and either
- /// one is correct.
- ///
- private static Action CreatePropertySetter(PropertyInfo property)
- {
- MethodInvoker? setMethodInvoker = null;
-
- return (entity, value) =>
- {
- setMethodInvoker ??= MethodInvoker.Create(property.SetMethod!);
-
- setMethodInvoker.Invoke(entity, value);
- };
- }
+ internal static void ResetEntityTypeMetadataCache() => entityTypeMetadataPerEntityType.Clear();
///
/// Creates the metadata for the entity type .
@@ -240,22 +192,24 @@ internal static void ResetEntityTypeMetadataCache() =>
/// There is more than one identity property defined for the entity type .
///
private static EntityTypeMetadata CreateEntityTypeMetadata(
- [DynamicallyAccessedMembers(EntityMemberTypes)] Type entityType)
+ [DynamicallyAccessedMembers(EntityMemberTypes)] Type entityType
+ )
{
- String tableName;
+ string tableName;
- DbConnectionPlusConfiguration.Instance.GetEntityTypeBuilders()
+ DbConnectionPlusConfiguration
+ .Instance.GetEntityTypeBuilders()
.TryGetValue(entityType, out var entityTypeBuilder);
if (entityTypeBuilder is not null)
{
- tableName = !String.IsNullOrWhiteSpace(entityTypeBuilder.TableName)
+ tableName = !string.IsNullOrWhiteSpace(entityTypeBuilder.TableName)
? entityTypeBuilder.TableName
: entityType.Name;
}
else
{
- tableName = !String.IsNullOrWhiteSpace(entityType.GetCustomAttribute()?.Name)
+ tableName = !string.IsNullOrWhiteSpace(entityType.GetCustomAttribute()?.Name)
? entityType.GetCustomAttribute()?.Name!
: entityType.Name;
}
@@ -268,16 +222,14 @@ private static EntityTypeMetadata CreateEntityTypeMetadata(
var property = properties[i];
if (
- entityTypeBuilder is not null &&
- entityTypeBuilder.PropertyBuilders.TryGetValue(property.Name, out var propertyBuilder)
+ entityTypeBuilder is not null
+ && entityTypeBuilder.PropertyBuilders.TryGetValue(property.Name, out var propertyBuilder)
)
{
propertiesMetadata[i] = new(
property.CanRead,
property.CanWrite,
- !String.IsNullOrWhiteSpace(propertyBuilder.ColumnName)
- ? propertyBuilder.ColumnName
- : property.Name,
+ !string.IsNullOrWhiteSpace(propertyBuilder.ColumnName) ? propertyBuilder.ColumnName : property.Name,
propertyBuilder.IsComputed,
propertyBuilder.IsConcurrencyToken,
propertyBuilder.IsIdentity,
@@ -297,11 +249,11 @@ entityTypeBuilder is not null &&
property.CanRead,
property.CanWrite,
property.GetCustomAttribute()?.Name ?? property.Name,
- property.GetCustomAttribute()?.DatabaseGeneratedOption is
- DatabaseGeneratedOption.Computed,
+ property.GetCustomAttribute()?.DatabaseGeneratedOption
+ is DatabaseGeneratedOption.Computed,
property.GetCustomAttribute() is not null,
- property.GetCustomAttribute()?.DatabaseGeneratedOption is
- DatabaseGeneratedOption.Identity,
+ property.GetCustomAttribute()?.DatabaseGeneratedOption
+ is DatabaseGeneratedOption.Identity,
property.GetCustomAttribute() is not null,
property.GetCustomAttribute() is not null,
property.GetCustomAttribute() is not null,
@@ -319,48 +271,58 @@ entityTypeBuilder is not null &&
if (identityProperties.Count > 1)
{
throw new InvalidOperationException(
- $"There are multiple identity properties defined for the entity type {entityType}. Only one property " +
- "can be marked as an identity property per entity type."
+ $"There are multiple identity properties defined for the entity type {entityType}. Only one property "
+ + "can be marked as an identity property per entity type."
);
}
IReadOnlyList computedProperties =
- [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsComputed: true })];
+ [
+ .. propertiesMetadata.Where(p => p is { IsIgnored: false, IsComputed: true }),
+ ];
IReadOnlyList concurrencyTokenProperties =
- [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsConcurrencyToken: true })];
+ [
+ .. propertiesMetadata.Where(p => p is { IsIgnored: false, IsConcurrencyToken: true }),
+ ];
IReadOnlyList databaseGeneratedProperties =
- [.. propertiesMetadata.Where(p => !p.IsIgnored && (p.IsComputed || p.IsIdentity || p.IsRowVersion))];
+ [
+ .. propertiesMetadata.Where(p => !p.IsIgnored && (p.IsComputed || p.IsIdentity || p.IsRowVersion)),
+ ];
IReadOnlyList insertProperties =
[
- .. propertiesMetadata.Where(p => p is
- { IsIgnored: false, IsComputed: false, IsIdentity: false, IsRowVersion: false }
- )
+ .. propertiesMetadata.Where(p =>
+ p is { IsIgnored: false, IsComputed: false, IsIdentity: false, IsRowVersion: false }
+ ),
];
IReadOnlyList keyProperties =
- [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsKey: true })];
+ [
+ .. propertiesMetadata.Where(p => p is { IsIgnored: false, IsKey: true }),
+ ];
- IReadOnlyList mappedProperties =
- [.. propertiesMetadata.Where(p => !p.IsIgnored)];
+ IReadOnlyList mappedProperties = [.. propertiesMetadata.Where(p => !p.IsIgnored)];
IReadOnlyList rowVersionProperties =
- [.. propertiesMetadata.Where(p => p is { IsIgnored: false, IsRowVersion: true })];
+ [
+ .. propertiesMetadata.Where(p => p is { IsIgnored: false, IsRowVersion: true }),
+ ];
IReadOnlyList updateProperties =
[
- .. propertiesMetadata.Where(p => p is
- {
- IsComputed: false,
- IsConcurrencyToken: false,
- IsIgnored: false,
- IsIdentity: false,
- IsKey: false,
- IsRowVersion: false
- }
- )
+ .. propertiesMetadata.Where(p =>
+ p
+ is {
+ IsComputed: false,
+ IsConcurrencyToken: false,
+ IsIgnored: false,
+ IsIdentity: false,
+ IsKey: false,
+ IsRowVersion: false
+ }
+ ),
];
return new(
@@ -380,6 +342,49 @@ .. propertiesMetadata.Where(p => p is
);
}
- private static readonly
- ConcurrentDictionary entityTypeMetadataPerEntityType = [];
+ ///
+ /// Creates the getter function for the property .
+ ///
+ /// The property for which to create the getter function.
+ /// A function taking an entity and returning the value of .
+ ///
+ /// The underlying is resolved on the first call and then kept in the closure, so
+ /// building the metadata of an entity type costs nothing per property until an accessor is actually used. The
+ /// unsynchronized assignment is deliberate: two threads racing here produce two equivalent invokers, and either
+ /// one is correct.
+ ///
+ private static Func CreatePropertyGetter(PropertyInfo property)
+ {
+ MethodInvoker? getMethodInvoker = null;
+
+ return entity =>
+ {
+ getMethodInvoker ??= MethodInvoker.Create(property.GetMethod!);
+
+ return getMethodInvoker.Invoke(entity);
+ };
+ }
+
+ ///
+ /// Creates the setter function for the property .
+ ///
+ /// The property for which to create the setter function.
+ /// An action taking an entity and the value to assign to .
+ ///
+ /// The underlying is resolved on the first call and then kept in the closure, so
+ /// building the metadata of an entity type costs nothing per property until an accessor is actually used. The
+ /// unsynchronized assignment is deliberate: two threads racing here produce two equivalent invokers, and either
+ /// one is correct.
+ ///
+ private static Action CreatePropertySetter(PropertyInfo property)
+ {
+ MethodInvoker? setMethodInvoker = null;
+
+ return (entity, value) =>
+ {
+ setMethodInvoker ??= MethodInvoker.Create(property.SetMethod!);
+
+ setMethodInvoker.Invoke(entity, value);
+ };
+ }
}
diff --git a/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs b/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs
index f64b393..548567f 100644
--- a/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs
+++ b/src/DbConnectionPlus/Entities/EntityPropertyMetadata.cs
@@ -33,18 +33,18 @@ namespace RentADeveloper.DbConnectionPlus.Entities;
///
/// The property type of the property.
public sealed record EntityPropertyMetadata(
- Boolean CanRead,
- Boolean CanWrite,
- String ColumnName,
- Boolean IsComputed,
- Boolean IsConcurrencyToken,
- Boolean IsIdentity,
- Boolean IsIgnored,
- Boolean IsKey,
- Boolean IsRowVersion,
- Func? PropertyGetter,
+ bool CanRead,
+ bool CanWrite,
+ string ColumnName,
+ bool IsComputed,
+ bool IsConcurrencyToken,
+ bool IsIdentity,
+ bool IsIgnored,
+ bool IsKey,
+ bool IsRowVersion,
+ Func? PropertyGetter,
PropertyInfo PropertyInfo,
- String PropertyName,
- Action? PropertySetter,
+ string PropertyName,
+ Action? PropertySetter,
Type PropertyType
);
diff --git a/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs b/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs
index 59cd10b..5202b2c 100644
--- a/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs
+++ b/src/DbConnectionPlus/Entities/EntityTypeMetadata.cs
@@ -44,9 +44,9 @@ namespace RentADeveloper.DbConnectionPlus.Entities;
///
public sealed record EntityTypeMetadata(
Type EntityType,
- String TableName,
+ string TableName,
IReadOnlyList AllProperties,
- IReadOnlyDictionary AllPropertiesByPropertyName,
+ IReadOnlyDictionary AllPropertiesByPropertyName,
IReadOnlyList ComputedProperties,
IReadOnlyList ConcurrencyTokenProperties,
IReadOnlyList DatabaseGeneratedProperties,
diff --git a/src/DbConnectionPlus/EnumSerializationMode.cs b/src/DbConnectionPlus/EnumSerializationMode.cs
index 20e11a9..0aaa3d4 100644
--- a/src/DbConnectionPlus/EnumSerializationMode.cs
+++ b/src/DbConnectionPlus/EnumSerializationMode.cs
@@ -16,5 +16,5 @@ public enum EnumSerializationMode
///
/// values are serialized as strings.
///
- Strings = 1
+ Strings = 1,
}
diff --git a/src/DbConnectionPlus/Exceptions/DbUpdateConcurrencyException.cs b/src/DbConnectionPlus/Exceptions/DbUpdateConcurrencyException.cs
index 01b2de5..0298823 100644
--- a/src/DbConnectionPlus/Exceptions/DbUpdateConcurrencyException.cs
+++ b/src/DbConnectionPlus/Exceptions/DbUpdateConcurrencyException.cs
@@ -12,35 +12,31 @@ public class DbUpdateConcurrencyException : Exception
///
/// The error message.
/// The entity that was involved in the concurrency violation.
- public DbUpdateConcurrencyException(String message, Object entity) : base(message) =>
- this.Entity = entity;
+ public DbUpdateConcurrencyException(string message, object entity)
+ : base(message) => this.Entity = entity;
///
/// Initializes a new instance of the class.
///
- public DbUpdateConcurrencyException()
- {
- }
+ public DbUpdateConcurrencyException() { }
///
/// Initializes a new instance of the class.
///
/// The error message.
- public DbUpdateConcurrencyException(String message) : base(message)
- {
- }
+ public DbUpdateConcurrencyException(string message)
+ : base(message) { }
///
/// Initializes a new instance of the class.
///
/// The error message.
/// The inner exception.
- public DbUpdateConcurrencyException(String message, Exception innerException) : base(message, innerException)
- {
- }
+ public DbUpdateConcurrencyException(string message, Exception innerException)
+ : base(message, innerException) { }
///
/// The entity that was involved in the concurrency violation.
///
- public Object? Entity { get; set; }
+ public object? Entity { get; set; }
}
diff --git a/src/DbConnectionPlus/Extensions/DbDataReaderExtensions.cs b/src/DbConnectionPlus/Extensions/DbDataReaderExtensions.cs
index 55d660a..1a4298e 100644
--- a/src/DbConnectionPlus/Extensions/DbDataReaderExtensions.cs
+++ b/src/DbConnectionPlus/Extensions/DbDataReaderExtensions.cs
@@ -17,11 +17,11 @@ internal static class DbDataReaderExtensions
/// The order of names in the array corresponds to the order of the fields in .
///
/// is .
- internal static String[] GetFieldNames(this DbDataReader dataReader)
+ internal static string[] GetFieldNames(this DbDataReader dataReader)
{
ArgumentNullException.ThrowIfNull(dataReader);
- var result = new String[dataReader.FieldCount];
+ var result = new string[dataReader.FieldCount];
for (var i = 0; i < dataReader.FieldCount; i++)
{
diff --git a/src/DbConnectionPlus/Extensions/Int32Extensions.cs b/src/DbConnectionPlus/Extensions/Int32Extensions.cs
index d4eef46..4a3daf2 100644
--- a/src/DbConnectionPlus/Extensions/Int32Extensions.cs
+++ b/src/DbConnectionPlus/Extensions/Int32Extensions.cs
@@ -6,18 +6,17 @@
namespace RentADeveloper.DbConnectionPlus.Extensions;
///
-/// Provides extension methods for the type .
+/// Provides extension methods for the type .
///
internal static class Int32Extensions
{
+ private static readonly CultureInfo englishCulture = new("en-US");
+
///
/// Turns this number into an ordinal number in english notation, used to denote the position in an ordered sequence
/// (e.g. 1st, 2nd, 3rd, 4th).
///
/// The number to ordinalize.
/// The ordinalized number in english notation.
- internal static String OrdinalizeEnglish(this Int32 value) =>
- value.Ordinalize(englishCulture);
-
- private static readonly CultureInfo englishCulture = new("en-US");
+ internal static string OrdinalizeEnglish(this int value) => value.Ordinalize(englishCulture);
}
diff --git a/src/DbConnectionPlus/Extensions/ObjectExtensions.cs b/src/DbConnectionPlus/Extensions/ObjectExtensions.cs
index 30a0fca..2dc2a3d 100644
--- a/src/DbConnectionPlus/Extensions/ObjectExtensions.cs
+++ b/src/DbConnectionPlus/Extensions/ObjectExtensions.cs
@@ -4,10 +4,15 @@
namespace RentADeveloper.DbConnectionPlus.Extensions;
///
-/// Provides extension methods for the type .
+/// Provides extension methods for the type .
///
internal static class ObjectExtensions
{
+ ///
+ /// The deepest sequence nesting that is rendered before the representation is truncated.
+ ///
+ private const int MaxSequenceDepth = 10;
+
///
/// Gets the string representation of this value suffixed by the fullname of this value's type.
///
@@ -17,124 +22,93 @@ internal static class ObjectExtensions
///
///
/// Sequences are rendered element by element as [a,b,c]; a value of any other unhandled type is
- /// rendered via . Nothing here reflects over the value, so the whole path stays
+ /// rendered via . Nothing here reflects over the value, so the whole path stays
/// usable under Native AOT and trimming — which matters, because this method builds the message of every
/// conversion failure and must not fail while doing so.
///
- internal static String ToDebugString(this Object? value) =>
+ internal static string ToDebugString(this object? value) =>
value switch
{
null => "{null}",
DBNull => "{DBNull}",
- _ => $"'{FormatValue(value, 0)}' ({value.GetType()})"
+ _ => $"'{FormatValue(value, 0)}' ({value.GetType()})",
};
+ ///
+ /// Gets the string representation of a sequence, as its elements separated by commas in square brackets.
+ ///
+ /// The sequence of which to get the string representation.
+ /// The nesting depth at which itself sits.
+ /// A string representation of .
+ private static string FormatSequence(IEnumerable values, int depth) =>
+ depth >= MaxSequenceDepth
+ ? "[...]"
+ : "[" + string.Join(",", values.Cast().Select(item => FormatValue(item, depth + 1))) + "]";
+
///
/// Gets the string representation of a value, without the type suffix.
///
/// The value of which to get the string representation.
/// The current nesting depth, used to bound the recursion into nested sequences.
/// A string representation of .
- private static String FormatValue(Object? value, Int32 depth) =>
+ private static string FormatValue(object? value, int depth) =>
value switch
{
- null =>
- "{null}",
+ null => "{null}",
- DBNull =>
- "{DBNull}",
+ DBNull => "{DBNull}",
- Boolean booleanValue =>
- booleanValue ? "True" : "False",
+ bool booleanValue => booleanValue ? "True" : "False",
- Byte byteValue =>
- byteValue.ToString("G", CultureInfo.InvariantCulture),
+ byte byteValue => byteValue.ToString("G", CultureInfo.InvariantCulture),
- Byte[] bytesValue =>
- Convert.ToBase64String(bytesValue),
+ byte[] bytesValue => Convert.ToBase64String(bytesValue),
- Char charValue =>
- charValue.ToString(),
+ char charValue => charValue.ToString(),
- DateTime dateTimeValue =>
- dateTimeValue.ToString("O", CultureInfo.InvariantCulture),
+ DateTime dateTimeValue => dateTimeValue.ToString("O", CultureInfo.InvariantCulture),
- DateTimeOffset dateTimeOffsetValue =>
- dateTimeOffsetValue.ToString("O", CultureInfo.InvariantCulture),
+ DateTimeOffset dateTimeOffsetValue => dateTimeOffsetValue.ToString("O", CultureInfo.InvariantCulture),
- Decimal decimalValue =>
- decimalValue.ToString("N", CultureInfo.InvariantCulture),
+ decimal decimalValue => decimalValue.ToString("N", CultureInfo.InvariantCulture),
- Double doubleValue =>
- doubleValue.ToString("G17", CultureInfo.InvariantCulture),
+ double doubleValue => doubleValue.ToString("G17", CultureInfo.InvariantCulture),
- Enum enumValue =>
- enumValue.ToString(),
+ Enum enumValue => enumValue.ToString(),
- Guid guidValue =>
- guidValue.ToString("D", CultureInfo.InvariantCulture),
+ Guid guidValue => guidValue.ToString("D", CultureInfo.InvariantCulture),
- Int16 int16Value =>
- int16Value.ToString("G", CultureInfo.InvariantCulture),
+ short int16Value => int16Value.ToString("G", CultureInfo.InvariantCulture),
- Int32 int32Value =>
- int32Value.ToString("G", CultureInfo.InvariantCulture),
+ int int32Value => int32Value.ToString("G", CultureInfo.InvariantCulture),
- Int64 int64Value =>
- int64Value.ToString("G", CultureInfo.InvariantCulture),
+ long int64Value => int64Value.ToString("G", CultureInfo.InvariantCulture),
- IntPtr intPtrValue =>
- intPtrValue.ToString("G", CultureInfo.InvariantCulture),
+ nint intPtrValue => intPtrValue.ToString("G", CultureInfo.InvariantCulture),
- SByte sbyteValue =>
- sbyteValue.ToString("G", CultureInfo.InvariantCulture),
+ sbyte sbyteValue => sbyteValue.ToString("G", CultureInfo.InvariantCulture),
- Single singleValue =>
- singleValue.ToString("G9", CultureInfo.InvariantCulture),
+ float singleValue => singleValue.ToString("G9", CultureInfo.InvariantCulture),
- String stringValue =>
- stringValue,
+ string stringValue => stringValue,
- TimeSpan timeSpanValue =>
- timeSpanValue.ToString("c", CultureInfo.InvariantCulture),
+ TimeSpan timeSpanValue => timeSpanValue.ToString("c", CultureInfo.InvariantCulture),
- UInt16 uint16Value =>
- uint16Value.ToString("G", CultureInfo.InvariantCulture),
+ ushort uint16Value => uint16Value.ToString("G", CultureInfo.InvariantCulture),
- UInt32 uint32Value =>
- uint32Value.ToString("G", CultureInfo.InvariantCulture),
+ uint uint32Value => uint32Value.ToString("G", CultureInfo.InvariantCulture),
- UInt64 uint64Value =>
- uint64Value.ToString("G", CultureInfo.InvariantCulture),
+ ulong uint64Value => uint64Value.ToString("G", CultureInfo.InvariantCulture),
- UIntPtr uintPtrValue =>
- uintPtrValue.ToString("G", CultureInfo.InvariantCulture),
+ nuint uintPtrValue => uintPtrValue.ToString("G", CultureInfo.InvariantCulture),
// Must stay below the Byte[] and String arms above, both of which are sequences that have
// a more useful representation of their own.
- IEnumerable sequenceValue =>
- FormatSequence(sequenceValue, depth),
+ IEnumerable sequenceValue => FormatSequence(sequenceValue, depth),
// Deliberately not JsonSerializer.Serialize: the reflection-based JsonSerializer overloads
// are unavailable under Native AOT, so a conversion error would itself fail while building
// its message. A type that renders as its own name here simply has no ToString override.
- _ =>
- value.ToString() ?? String.Empty
+ _ => value.ToString() ?? string.Empty,
};
-
- ///
- /// Gets the string representation of a sequence, as its elements separated by commas in square brackets.
- ///
- /// The sequence of which to get the string representation.
- /// The nesting depth at which itself sits.
- /// A string representation of .
- private static String FormatSequence(IEnumerable values, Int32 depth) =>
- depth >= MaxSequenceDepth
- ? "[...]"
- : "[" + String.Join(",", values.Cast().Select(item => FormatValue(item, depth + 1))) + "]";
-
- ///
- /// The deepest sequence nesting that is rendered before the representation is truncated.
- ///
- private const Int32 MaxSequenceDepth = 10;
}
diff --git a/src/DbConnectionPlus/Extensions/TypeExtensions.cs b/src/DbConnectionPlus/Extensions/TypeExtensions.cs
index b3e781a..5273972 100644
--- a/src/DbConnectionPlus/Extensions/TypeExtensions.cs
+++ b/src/DbConnectionPlus/Extensions/TypeExtensions.cs
@@ -8,9 +8,47 @@ namespace RentADeveloper.DbConnectionPlus.Extensions;
///
internal static class TypeExtensions
{
+ private static readonly HashSet builtInTypes =
+ [
+ typeof(bool),
+ typeof(byte),
+ typeof(sbyte),
+ typeof(char),
+ typeof(decimal),
+ typeof(double),
+ typeof(float),
+ typeof(short),
+ typeof(ushort),
+ typeof(int),
+ typeof(uint),
+ typeof(long),
+ typeof(ulong),
+ typeof(nint),
+ typeof(nuint),
+ typeof(string),
+ typeof(DateTime),
+ typeof(DateOnly),
+ typeof(DateTimeOffset),
+ typeof(TimeSpan),
+ typeof(TimeOnly),
+ typeof(Guid),
+ ];
+
+ private static readonly HashSet valueTupleTypes =
+ [
+ typeof(ValueTuple<>),
+ typeof(ValueTuple<,>),
+ typeof(ValueTuple<,,>),
+ typeof(ValueTuple<,,,>),
+ typeof(ValueTuple<,,,,>),
+ typeof(ValueTuple<,,,,,>),
+ typeof(ValueTuple<,,,,,,>),
+ typeof(ValueTuple<,,,,,,,>),
+ ];
+
///
/// Determines whether this type is a built-in .NET type
- /// (e.g. , , , ...).
+ /// (e.g. , , , ...).
///
/// The type to inspect.
///
@@ -18,7 +56,7 @@ internal static class TypeExtensions
///
/// is .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static Boolean IsBuiltInTypeOrNullableBuiltInType(this Type type)
+ internal static bool IsBuiltInTypeOrNullableBuiltInType(this Type type)
{
ArgumentNullException.ThrowIfNull(type);
@@ -26,20 +64,20 @@ internal static Boolean IsBuiltInTypeOrNullableBuiltInType(this Type type)
}
///
- /// Determines whether this type is or .
+ /// Determines whether this type is or .
///
/// The type to inspect.
///
- /// if this type is or ; otherwise,
+ /// if this type is or ; otherwise,
/// .
///
/// is .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static Boolean IsCharOrNullableCharType(this Type type)
+ internal static bool IsCharOrNullableCharType(this Type type)
{
ArgumentNullException.ThrowIfNull(type);
- return type == typeof(Char) || type == typeof(Char?);
+ return type == typeof(char) || type == typeof(char?);
}
///
@@ -52,7 +90,7 @@ internal static Boolean IsCharOrNullableCharType(this Type type)
///
/// is .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static Boolean IsEnumOrNullableEnumType(this Type type)
+ internal static bool IsEnumOrNullableEnumType(this Type type)
{
ArgumentNullException.ThrowIfNull(type);
@@ -69,7 +107,7 @@ internal static Boolean IsEnumOrNullableEnumType(this Type type)
///
/// is .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static Boolean IsReferenceTypeOrNullableType(this Type type)
+ internal static bool IsReferenceTypeOrNullableType(this Type type)
{
ArgumentNullException.ThrowIfNull(type);
@@ -86,48 +124,10 @@ internal static Boolean IsReferenceTypeOrNullableType(this Type type)
///
/// is .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static Boolean IsValueTupleType(this Type type)
+ internal static bool IsValueTupleType(this Type type)
{
ArgumentNullException.ThrowIfNull(type);
return type.IsGenericType && valueTupleTypes.Contains(type.GetGenericTypeDefinition());
}
-
- private static readonly HashSet builtInTypes =
- [
- typeof(Boolean),
- typeof(Byte),
- typeof(SByte),
- typeof(Char),
- typeof(Decimal),
- typeof(Double),
- typeof(Single),
- typeof(Int16),
- typeof(UInt16),
- typeof(Int32),
- typeof(UInt32),
- typeof(Int64),
- typeof(UInt64),
- typeof(IntPtr),
- typeof(UIntPtr),
- typeof(String),
- typeof(DateTime),
- typeof(DateOnly),
- typeof(DateTimeOffset),
- typeof(TimeSpan),
- typeof(TimeOnly),
- typeof(Guid)
- ];
-
- private static readonly HashSet valueTupleTypes =
- [
- typeof(ValueTuple<>),
- typeof(ValueTuple<,>),
- typeof(ValueTuple<,,>),
- typeof(ValueTuple<,,,>),
- typeof(ValueTuple<,,,,>),
- typeof(ValueTuple<,,,,,>),
- typeof(ValueTuple<,,,,,,>),
- typeof(ValueTuple<,,,,,,,>)
- ];
}
diff --git a/src/DbConnectionPlus/Helpers/NameHelper.cs b/src/DbConnectionPlus/Helpers/NameHelper.cs
index 790f18f..769d897 100644
--- a/src/DbConnectionPlus/Helpers/NameHelper.cs
+++ b/src/DbConnectionPlus/Helpers/NameHelper.cs
@@ -24,7 +24,7 @@ internal static class NameHelper
/// The first character of the resulting name is converted to uppercase if it is a lowercase letter.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static String CreateNameFromCallerArgumentExpression(ReadOnlySpan expression, Int32 maximumLength)
+ internal static string CreateNameFromCallerArgumentExpression(ReadOnlySpan expression, int maximumLength)
{
// Remove common prefixes:
@@ -45,7 +45,7 @@ internal static String CreateNameFromCallerArgumentExpression(ReadOnlySpan
var bufferLength = Math.Min(expression.Length, maximumLength);
- var buffer = bufferLength <= 512 ? stackalloc Char[bufferLength] : new Char[bufferLength];
+ var buffer = bufferLength <= 512 ? stackalloc char[bufferLength] : new char[bufferLength];
ref var expressionPointer = ref MemoryMarshal.GetReference(expression);
ref var bufferPointer = ref MemoryMarshal.GetReference(buffer);
@@ -57,9 +57,12 @@ internal static String CreateNameFromCallerArgumentExpression(ReadOnlySpan
var character = Unsafe.Add(ref expressionPointer, i);
if (
- (UInt32)(character - '0') <= 9 || // Digits
- (UInt32)(character - 'A') <= 25 || // Uppercase letters
- (UInt32)(character - 'a') <= 25 || // Lowercase letters
+ (uint)(character - '0') <= 9
+ || // Digits
+ (uint)(character - 'A') <= 25
+ || // Uppercase letters
+ (uint)(character - 'a') <= 25
+ || // Lowercase letters
character == '_'
)
{
@@ -73,9 +76,9 @@ internal static String CreateNameFromCallerArgumentExpression(ReadOnlySpan
}
// Convert the first character to uppercase if necessary.
- if (count != 0 && (UInt32)(buffer[0] - 'a') <= 25)
+ if (count != 0 && (uint)(buffer[0] - 'a') <= 25)
{
- buffer[0] = (Char)(buffer[0] - 32);
+ buffer[0] = (char)(buffer[0] - 32);
}
return new(buffer[..count]);
diff --git a/src/DbConnectionPlus/Materializers/DataRowMaterializer.cs b/src/DbConnectionPlus/Materializers/DataRowMaterializer.cs
index 3c38479..9e09ada 100644
--- a/src/DbConnectionPlus/Materializers/DataRowMaterializer.cs
+++ b/src/DbConnectionPlus/Materializers/DataRowMaterializer.cs
@@ -20,10 +20,10 @@ internal static DataRow Materialize(DbDataReader dataReader)
{
ArgumentNullException.ThrowIfNull(dataReader);
- var values = new Object[dataReader.FieldCount];
+ var values = new object[dataReader.FieldCount];
dataReader.GetValues(values);
- var columns = new Dictionary(dataReader.FieldCount);
+ var columns = new Dictionary(dataReader.FieldCount);
for (var ordinal = 0; ordinal < dataReader.FieldCount; ordinal++)
{
diff --git a/src/DbConnectionPlus/Materializers/EntityMaterializerFactory.cs b/src/DbConnectionPlus/Materializers/EntityMaterializerFactory.cs
index 012a6c0..12a2f9f 100644
--- a/src/DbConnectionPlus/Materializers/EntityMaterializerFactory.cs
+++ b/src/DbConnectionPlus/Materializers/EntityMaterializerFactory.cs
@@ -4,7 +4,6 @@
using System.Linq.Expressions;
using System.Reflection;
using RentADeveloper.DbConnectionPlus.Converters;
-using RentADeveloper.DbConnectionPlus.Entities;
using RentADeveloper.DbConnectionPlus.Extensions;
namespace RentADeveloper.DbConnectionPlus.Materializers;
@@ -24,10 +23,90 @@ internal static class EntityMaterializerFactory
/// branch; the attribute exists so that the analyzer
/// verifies that guard rather than so that a warning propagates.
///
- private const String MaterializerRequiresDynamicCodeMessage =
- "Materializing entities compiles an expression tree at run time, which is not supported when the " +
- "application is published with Native AOT. Reach this only from a RuntimeFeature.IsDynamicCodeSupported " +
- "branch.";
+ private const string MaterializerRequiresDynamicCodeMessage =
+ "Materializing entities compiles an expression tree at run time, which is not supported when the "
+ + "application is published with Native AOT. Reach this only from a RuntimeFeature.IsDynamicCodeSupported "
+ + "branch.";
+
+ private static readonly ConcurrentDictionary materializerCache = [];
+
+ ///
+ /// Creates a materializer function that materializes the data in a to an instance of
+ /// the type using reflection instead of a compiled expression tree.
+ ///
+ /// The type of entity to materialize.
+ /// The for which to create the materializer function.
+ ///
+ /// The names of the fields in .
+ /// The order of the names must match the order of the fields in .
+ ///
+ ///
+ /// The field types of the fields in .
+ /// The order of the types must match the order of the fields in .
+ ///
+ ///
+ /// A function that materializes the data in a to an instance of the type
+ /// .
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// This is the materializer for applications published with Native AOT, where no run-time code generation is
+ /// available. It mirrors the two strategies of and picks
+ /// between them the same way: constructor injection when the type has a constructor that matches the result
+ /// set, and a parameterless constructor followed by property setters otherwise.
+ ///
+ ///
+ /// Everything that depends only on the shape of the result set - the field ordinals, the target types, whether a
+ /// field value needs to be converted, and the property setters - is resolved once, exactly as the compiled
+ /// expression tree bakes it in. Per row the materializer only walks an array, reads the fields and writes them.
+ /// The materializer cache is keyed by that shape, so the resolution happens once per shape.
+ ///
+ ///
+ /// The exception types and the exception messages are identical to the ones of the compiled expression tree, so
+ /// that the behaviour a consumer observes does not depend on how the application was published.
+ ///
+ ///
+ internal static Func CreateReflectionMaterializer<
+ [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
+ >(DbDataReader dataReader, string[] dataReaderFieldNames, Type[] dataReaderFieldTypes)
+ {
+ ArgumentNullException.ThrowIfNull(dataReader);
+ ArgumentNullException.ThrowIfNull(dataReaderFieldNames);
+ ArgumentNullException.ThrowIfNull(dataReaderFieldTypes);
+
+ var compatibleConstructor = EntityHelper.FindCompatibleConstructor(
+ typeof(TEntity),
+ [.. dataReaderFieldNames.Zip(dataReaderFieldTypes, (name, type) => (name, type))]
+ );
+
+ return compatibleConstructor is null
+ ? CreateReflectionPropertyMaterializer(dataReader, dataReaderFieldNames, dataReaderFieldTypes)
+ : CreateReflectionConstructorMaterializer(
+ dataReader,
+ dataReaderFieldNames,
+ dataReaderFieldTypes,
+ compatibleConstructor
+ );
+ }
///
/// Gets a materializer function that materializes the data in a to an instance of the
@@ -128,7 +207,7 @@ internal static Func GetMaterializer<
///
/// Creates a materializer function that materializes the data in a to an instance of
- /// the type using reflection instead of a compiled expression tree.
+ /// the type by compiling an expression tree.
///
/// The type of entity to materialize.
/// The for which to create the materializer function.
@@ -144,285 +223,240 @@ internal static Func GetMaterializer<
/// A function that materializes the data in a to an instance of the type
/// .
///
- ///
- ///
- ///
- ///
- /// is .
- ///
- ///
- ///
- ///
- /// is .
- ///
- ///
- ///
- ///
- /// is .
- ///
- ///
- ///
- ///
+ ///
///
- ///
- /// This is the materializer for applications published with Native AOT, where no run-time code generation is
- /// available. It mirrors the two strategies of and picks
- /// between them the same way: constructor injection when the type has a constructor that matches the result
- /// set, and a parameterless constructor followed by property setters otherwise.
- ///
- ///
- /// Everything that depends only on the shape of the result set - the field ordinals, the target types, whether a
- /// field value needs to be converted, and the property setters - is resolved once, exactly as the compiled
- /// expression tree bakes it in. Per row the materializer only walks an array, reads the fields and writes them.
- /// The materializer cache is keyed by that shape, so the resolution happens once per shape.
- ///
- ///
- /// The exception types and the exception messages are identical to the ones of the compiled expression tree, so
- /// that the behaviour a consumer observes does not depend on how the application was published.
- ///
+ /// This is the fast path for runtimes that can generate code, and the only implementation the library had
+ /// before it grew a Native AOT counterpart. It is reached exclusively through the
+ /// check in ;
+ /// is the counterpart that produces the same results
+ /// without generating code.
///
- internal static Func CreateReflectionMaterializer<
+ [RequiresDynamicCode(MaterializerRequiresDynamicCodeMessage)]
+ private static Delegate CreateExpressionMaterializer<
[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
- DbDataReader dataReader,
- String[] dataReaderFieldNames,
- Type[] dataReaderFieldTypes
- )
+ >(DbDataReader dataReader, string[] dataReaderFieldNames, Type[] dataReaderFieldTypes)
{
- ArgumentNullException.ThrowIfNull(dataReader);
- ArgumentNullException.ThrowIfNull(dataReaderFieldNames);
- ArgumentNullException.ThrowIfNull(dataReaderFieldTypes);
+ var entityType = typeof(TEntity);
+
+ /*
+ * This method creates an expression tree to generate a materializer function instead of using reflection for
+ * the materialization, because using reflection would be significantly slower.
+ * Using expression trees also allows us to use the typed GetXXX methods of DbDataReader, which avoids boxing
+ * in many cases.
+ */
+
+ var dataReaderParameterExpression = Expression.Parameter(typeof(DbDataReader), "dataReader");
+ var dataReaderFieldValueExpressions = new Expression[dataReader.FieldCount];
+
+ var fieldOrdinalToTargetType = new Dictionary(dataReader.FieldCount);
+ var fieldOrdinalToConstructorParameterIndex = new Dictionary(dataReader.FieldCount);
var compatibleConstructor = EntityHelper.FindCompatibleConstructor(
- typeof(TEntity),
- [
- .. dataReaderFieldNames.Zip(dataReaderFieldTypes, (name, type) => (name, type))
- ]
+ entityType,
+ [.. dataReaderFieldNames.Zip(dataReaderFieldTypes, (name, type) => (name, type))]
);
- return compatibleConstructor is null
- ? CreateReflectionPropertyMaterializer(
- dataReader,
- dataReaderFieldNames,
- dataReaderFieldTypes
- )
- : CreateReflectionConstructorMaterializer(
- dataReader,
- dataReaderFieldNames,
- dataReaderFieldTypes,
- compatibleConstructor
- );
- }
+ var entityPropertiesByColumnName = EntityHelper
+ .GetEntityTypeMetadata(entityType)
+ .MappedProperties.Where(a => a.CanWrite)
+ .ToDictionary(a => a.ColumnName, StringComparer.OrdinalIgnoreCase);
- ///
- /// Creates a reflection-based materializer function that materializes the data in a
- /// to an instance of the type by passing the fields of the result set to
- /// .
- ///
- /// The type of entity to materialize.
- /// The for which to create the materializer function.
- ///
- /// The names of the fields in .
- /// The order of the names must match the order of the fields in .
- ///
- ///
- /// The field types of the fields in .
- /// The order of the types must match the order of the fields in .
- ///
- ///
- /// The constructor of the type whose parameters match the fields of the result
- /// set, as returned by .
- ///
- ///
- /// A function that materializes the data in a to an instance of the type
- /// .
- ///
- ///
- /// This is the strategy that materializes entities using constructor injection. Each field of the result set is
- /// matched to the constructor parameter of the same name, exactly as the compiled expression tree matches it,
- /// and the resulting bindings are stored in constructor-argument order. Per row the arguments are therefore
- /// read left to right, which is also the order in which the expression tree evaluates them - so when more than
- /// one field is unusable, both paths report the same one.
- ///
- private static Func CreateReflectionConstructorMaterializer(
- DbDataReader dataReader,
- String[] dataReaderFieldNames,
- Type[] dataReaderFieldTypes,
- ConstructorInfo compatibleConstructor
- )
- {
- var entityType = typeof(TEntity);
+ if (compatibleConstructor is not null)
+ {
+ var constructorParameters = compatibleConstructor.GetParameters().ToList();
- var constructorParameters = compatibleConstructor.GetParameters();
- var constructorArgumentBindings = new ReflectionColumnBinding[constructorParameters.Length];
+ for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
+ {
+ var constructorParameter = constructorParameters.First(p =>
+ !string.IsNullOrWhiteSpace(p.Name)
+ && p.Name.Equals(dataReaderFieldNames[fieldOrdinal], StringComparison.OrdinalIgnoreCase)
+ && ValueConverter.CanConvert(dataReaderFieldTypes[fieldOrdinal], p.ParameterType)
+ );
+
+ fieldOrdinalToConstructorParameterIndex.Add(
+ fieldOrdinal,
+ constructorParameters.IndexOf(constructorParameter)
+ );
+
+ fieldOrdinalToTargetType.Add(fieldOrdinal, constructorParameter.ParameterType);
+ }
+ }
+ else
+ {
+ for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
+ {
+ var dataReaderFieldName = dataReaderFieldNames[fieldOrdinal];
+
+ if (entityPropertiesByColumnName.TryGetValue(dataReaderFieldName, out var entityProperty))
+ {
+ fieldOrdinalToTargetType.Add(fieldOrdinal, entityProperty.PropertyType);
+ }
+ else
+ {
+ fieldOrdinalToTargetType.Add(fieldOrdinal, dataReaderFieldTypes[fieldOrdinal]);
+ }
+ }
+ }
for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
{
+ var fieldOrdinalExpression = Expression.Constant(fieldOrdinal);
+
var dataReaderFieldName = dataReaderFieldNames[fieldOrdinal];
+
+ if (compatibleConstructor is null && !entityPropertiesByColumnName.ContainsKey(dataReaderFieldName))
+ {
+ // No need to read the field when we are using properties to materialize and there is no matching
+ // property for the field.
+ continue;
+ }
+
var dataReaderFieldType = dataReaderFieldTypes[fieldOrdinal];
+ var targetType = fieldOrdinalToTargetType[fieldOrdinal];
- var constructorParameter = constructorParameters.First(p =>
- !String.IsNullOrWhiteSpace(p.Name) &&
- p.Name.Equals(dataReaderFieldName, StringComparison.OrdinalIgnoreCase) &&
- ValueConverter.CanConvert(dataReaderFieldType, p.ParameterType)
+ // Basically:
+ // dataReader.GetXXX(fieldOrdinal)
+ var getFieldValueCallExpression = MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueExpression(
+ dataReaderParameterExpression,
+ fieldOrdinalExpression,
+ fieldOrdinal,
+ dataReaderFieldName,
+ dataReaderFieldType
);
- constructorArgumentBindings[Array.IndexOf(constructorParameters, constructorParameter)] =
- new ReflectionColumnBinding(
- dataReaderFieldName,
- fieldOrdinal,
- MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction(
- fieldOrdinal,
- dataReaderFieldName,
- dataReaderFieldType
+ /*
+ * Basically:
+ *
+ * if (dataReader.IsDBNull(fieldOrdinal))
+ * {
+ * if (targetType.IsReferenceTypeOrNullableType())
+ * {
+ * default(targetType);
+ * }
+ * else
+ * {
+ * throw new InvalidCastException(...);
+ * }
+ * }
+ * else
+ * {
+ * if (dataReaderFieldType != targetType)
+ * {
+ * try
+ * {
+ * ValueConverter.ConvertValueToType((Object) dataReader.GetXXX(fieldOrdinal));
+ * }
+ * catch (Exception ex)
+ * {
+ * throw new InvalidCastException(..., ex);
+ * }
+ * }
+ * else
+ * {
+ * dataReader.GetXXX(fieldOrdinal);
+ * }
+ * }
+ */
+
+ var exceptionParameterExpression = Expression.Parameter(typeof(Exception));
+
+ Expression isDbNullBranchExpression = targetType.IsReferenceTypeOrNullableType()
+ ? Expression.Default(targetType)
+ : Expression.Throw(
+ Expression.New(
+ typeof(InvalidCastException).GetConstructor([typeof(string)])!,
+ Expression.Constant(
+ $"The column '{dataReaderFieldName}' returned by the SQL statement contains a "
+ + $"NULL value, but the corresponding property of the type {entityType} is "
+ + "non-nullable."
+ )
),
- dataReaderFieldType != constructorParameter.ParameterType,
- constructorParameter.ParameterType
+ targetType
);
- }
- var entityConstructor = ConstructorInvoker.Create(compatibleConstructor);
-
- return rowDataReader => MaterializeEntityThroughConstructor(
- rowDataReader,
- entityType,
- entityConstructor,
- constructorArgumentBindings
- );
- }
+ var throwInvalidCastExceptionExpression = Expression.Throw(
+ Expression.New(
+ typeof(InvalidCastException).GetConstructor([typeof(string), typeof(Exception)])!,
+ Expression.Constant(
+ $"The column '{dataReaderFieldName}' returned by the SQL statement "
+ + $"contains a value that could not be converted to the type {targetType} "
+ + $"of the corresponding property of the type {entityType}. See inner "
+ + "exception for details."
+ ),
+ exceptionParameterExpression
+ ),
+ targetType
+ );
- ///
- /// Creates a reflection-based materializer function that materializes the data in a
- /// to an instance of the type by constructing it with its parameterless
- /// constructor and then writing each field of the result set to the property it maps to.
- ///
- /// The type of entity to materialize.
- /// The for which to create the materializer function.
- ///
- /// The names of the fields in .
- /// The order of the names must match the order of the fields in .
- ///
- ///
- /// The field types of the fields in .
- /// The order of the types must match the order of the fields in .
- ///
- ///
- /// A function that materializes the data in a to an instance of the type
- /// .
- ///
- ///
- /// Callers must have established that the type has a parameterless constructor
- /// and no constructor compatible with the result set - does both. Fields
- /// without a matching property are never read, which the compiled expression tree does as well.
- ///
- private static Func CreateReflectionPropertyMaterializer<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
- DbDataReader dataReader,
- String[] dataReaderFieldNames,
- Type[] dataReaderFieldTypes
- )
- {
- var entityType = typeof(TEntity);
+ var convertFieldValueExpression = Expression.TryCatch(
+ Expression.Convert(
+ Expression.Call(
+ null,
+ MaterializerFactoryHelper.MakeValueConverterConvertValueToTypeMethod(targetType),
+ Expression.Convert(getFieldValueCallExpression, typeof(object))
+ ),
+ targetType
+ ),
+ Expression.Catch(exceptionParameterExpression, throwInvalidCastExceptionExpression)
+ );
- var entityPropertiesByColumnName = EntityHelper.GetEntityTypeMetadata(entityType)
- .MappedProperties.Where(a => a.CanWrite)
- .ToDictionary(a => a.ColumnName, StringComparer.OrdinalIgnoreCase);
+ var isNotDbNullBranchExpression =
+ dataReaderFieldType != targetType ? convertFieldValueExpression : getFieldValueCallExpression;
- var entityConstructor = ConstructorInvoker.Create(EntityHelper.FindParameterlessConstructor(entityType)!);
+ dataReaderFieldValueExpressions[fieldOrdinal] = Expression.Condition(
+ Expression.Call(
+ dataReaderParameterExpression,
+ MaterializerFactoryHelper.DbDataReaderIsDBNullMethod,
+ fieldOrdinalExpression
+ ),
+ isDbNullBranchExpression,
+ isNotDbNullBranchExpression
+ );
+ }
- var propertyBindings = new List(dataReader.FieldCount);
+ Expression bodyExpression;
- for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
+ if (compatibleConstructor is not null)
{
- var dataReaderFieldName = dataReaderFieldNames[fieldOrdinal];
+ var constructorArgumentExpressions = new Expression[dataReader.FieldCount];
- if (!entityPropertiesByColumnName.TryGetValue(dataReaderFieldName, out var entityProperty))
+ for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
{
- // No need to read the field when there is no matching property for it. The expression tree skips
- // these fields as well, and tests assert that the field is never touched.
- continue;
- }
+ var constructorArgumentIndex = fieldOrdinalToConstructorParameterIndex[fieldOrdinal];
- var dataReaderFieldType = dataReaderFieldTypes[fieldOrdinal];
- var targetType = entityProperty.PropertyType;
+ constructorArgumentExpressions[constructorArgumentIndex] = dataReaderFieldValueExpressions[
+ fieldOrdinal
+ ];
+ }
- propertyBindings.Add(
- new ReflectionPropertyBinding(
- new ReflectionColumnBinding(
- dataReaderFieldName,
- fieldOrdinal,
- MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction(
- fieldOrdinal,
- dataReaderFieldName,
- dataReaderFieldType
- ),
- dataReaderFieldType != targetType,
- targetType
- ),
- // entityPropertiesByColumnName only contains writable properties, so the setter always exists.
- entityProperty.PropertySetter!
- )
- );
+ // Basically:
+ // new TEntity(constructorArgumentExpressions...)
+ bodyExpression = Expression.New(compatibleConstructor, constructorArgumentExpressions);
}
+ else
+ {
+ var memberBindings = new List();
- var resolvedPropertyBindings = propertyBindings.ToArray();
+ for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
+ {
+ var dataReaderFieldName = dataReaderFieldNames[fieldOrdinal];
- return rowDataReader => MaterializeEntityThroughProperties(
- rowDataReader,
- entityType,
- entityConstructor,
- resolvedPropertyBindings
- );
- }
+ if (!entityPropertiesByColumnName.TryGetValue(dataReaderFieldName, out var entityProperty))
+ {
+ continue;
+ }
- ///
- /// Throws if none of the fields of the result set can be mapped to a writable property of
- /// .
- ///
- /// The entity type the result set is being materialized to.
- /// The field names of the result set.
- /// The writable properties of the entity type, by column name.
- ///
- /// The result set has fields, but none of them maps to a writable property of .
- ///
- ///
- ///
- /// Without this check, materialization silently succeeds and returns entities whose properties are all left at
- /// their default values, which is indistinguishable from a query that legitimately returned default data.
- ///
- ///
- /// The check matters most in an application that is trimmed or published with Native AOT. If a
- /// annotation is missing anywhere on the call path, the
- /// trimmer removes the entity's properties, reflection then reports fewer members than the type really has, and
- /// every column silently fails to bind. Failing loudly here is the backstop for that.
- ///
- ///
- private static void GuardAgainstResultSetBindingNoProperties(
- Type entityType,
- String[] dataReaderFieldNames,
- Dictionary entityPropertiesByColumnName
- )
- {
- if (dataReaderFieldNames.Length == 0)
- {
- return;
- }
+ memberBindings.Add(
+ Expression.Bind(entityProperty.PropertyInfo, dataReaderFieldValueExpressions[fieldOrdinal])
+ );
+ }
- if (dataReaderFieldNames.Any(entityPropertiesByColumnName.ContainsKey))
- {
- return;
+ // Basically:
+ // new TEntity { Property1 = ..., Property2 = ..., ... }
+ bodyExpression = Expression.MemberInit(Expression.New(entityType), memberBindings);
}
- throw new InvalidOperationException(
- $"None of the {dataReaderFieldNames.Length} field(s) of the result set " +
- $"({String.Join(", ", dataReaderFieldNames)}) could be mapped to a writable property of the entity " +
- $"type {entityType}. Materializing the result set would return entities whose properties are all left " +
- "at their default values. Check that the field names of the result set match the property names, or " +
- "the mapped column names, of the entity type. If the application is trimmed or published with Native " +
- "AOT, this usually means the properties of the entity type were removed by the trimmer because a " +
- "[DynamicallyAccessedMembers] annotation is missing on the call path."
- );
+ return Expression.Lambda(bodyExpression, dataReaderParameterExpression).Compile();
}
///
@@ -489,18 +523,15 @@ Dictionary entityPropertiesByColumnName
[UnconditionalSuppressMessage(
"AOT",
"IL3050:Requires dynamic code",
- Justification =
- "The call is inside an if (RuntimeFeature.IsDynamicCodeSupported) branch, which the AOT compiler folds " +
- "to false and removes together with the expression-tree implementation. The net9.0+ analyzer " +
- "recognizes that guard and reports nothing here; net8.0 lacks the [FeatureGuard] annotation on " +
- "IsDynamicCodeSupported that lets it do so."
+ Justification = "The call is inside an if (RuntimeFeature.IsDynamicCodeSupported) branch, which the AOT compiler folds "
+ + "to false and removes together with the expression-tree implementation. The net9.0+ analyzer "
+ + "recognizes that guard and reports nothing here; net8.0 lacks the [FeatureGuard] annotation on "
+ + "IsDynamicCodeSupported that lets it do so."
)]
#endif
- private static Delegate CreateMaterializer<
- [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
+ private static Delegate CreateMaterializer<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>(
DbDataReader dataReader,
- String[] dataReaderFieldNames,
+ string[] dataReaderFieldNames,
Type[] dataReaderFieldTypes
)
{
@@ -519,7 +550,8 @@ [.. dataReaderFieldNames.Zip(dataReaderFieldTypes, (name, type) => (name, type))
GuardAgainstResultSetBindingNoProperties(
entityType,
dataReaderFieldNames,
- EntityHelper.GetEntityTypeMetadata(entityType)
+ EntityHelper
+ .GetEntityTypeMetadata(entityType)
.MappedProperties.Where(a => a.CanWrite)
.ToDictionary(a => a.ColumnName, StringComparer.OrdinalIgnoreCase)
);
@@ -534,8 +566,86 @@ [.. dataReaderFieldNames.Zip(dataReaderFieldTypes, (name, type) => (name, type))
}
///
- /// Creates a materializer function that materializes the data in a to an instance of
- /// the type by compiling an expression tree.
+ /// Creates a reflection-based materializer function that materializes the data in a
+ /// to an instance of the type by passing the fields of the result set to
+ /// .
+ ///
+ /// The type of entity to materialize.
+ /// The for which to create the materializer function.
+ ///
+ /// The names of the fields in .
+ /// The order of the names must match the order of the fields in .
+ ///
+ ///
+ /// The field types of the fields in .
+ /// The order of the types must match the order of the fields in .
+ ///
+ ///
+ /// The constructor of the type whose parameters match the fields of the result
+ /// set, as returned by .
+ ///
+ ///
+ /// A function that materializes the data in a to an instance of the type
+ /// .
+ ///
+ ///
+ /// This is the strategy that materializes entities using constructor injection. Each field of the result set is
+ /// matched to the constructor parameter of the same name, exactly as the compiled expression tree matches it,
+ /// and the resulting bindings are stored in constructor-argument order. Per row the arguments are therefore
+ /// read left to right, which is also the order in which the expression tree evaluates them - so when more than
+ /// one field is unusable, both paths report the same one.
+ ///
+ private static Func CreateReflectionConstructorMaterializer(
+ DbDataReader dataReader,
+ string[] dataReaderFieldNames,
+ Type[] dataReaderFieldTypes,
+ ConstructorInfo compatibleConstructor
+ )
+ {
+ var entityType = typeof(TEntity);
+
+ var constructorParameters = compatibleConstructor.GetParameters();
+ var constructorArgumentBindings = new ReflectionColumnBinding[constructorParameters.Length];
+
+ for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
+ {
+ var dataReaderFieldName = dataReaderFieldNames[fieldOrdinal];
+ var dataReaderFieldType = dataReaderFieldTypes[fieldOrdinal];
+
+ var constructorParameter = constructorParameters.First(p =>
+ !string.IsNullOrWhiteSpace(p.Name)
+ && p.Name.Equals(dataReaderFieldName, StringComparison.OrdinalIgnoreCase)
+ && ValueConverter.CanConvert(dataReaderFieldType, p.ParameterType)
+ );
+
+ constructorArgumentBindings[Array.IndexOf(constructorParameters, constructorParameter)] = new(
+ dataReaderFieldName,
+ fieldOrdinal,
+ MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction(
+ fieldOrdinal,
+ dataReaderFieldName,
+ dataReaderFieldType
+ ),
+ dataReaderFieldType != constructorParameter.ParameterType,
+ constructorParameter.ParameterType
+ );
+ }
+
+ var entityConstructor = ConstructorInvoker.Create(compatibleConstructor);
+
+ return rowDataReader =>
+ MaterializeEntityThroughConstructor(
+ rowDataReader,
+ entityType,
+ entityConstructor,
+ constructorArgumentBindings
+ );
+ }
+
+ ///
+ /// Creates a reflection-based materializer function that materializes the data in a
+ /// to an instance of the type by constructing it with its parameterless
+ /// constructor and then writing each field of the result set to the property it maps to.
///
/// The type of entity to materialize.
/// The for which to create the materializer function.
@@ -551,255 +661,117 @@ [.. dataReaderFieldNames.Zip(dataReaderFieldTypes, (name, type) => (name, type))
/// A function that materializes the data in a to an instance of the type
/// .
///
- ///
///
- /// This is the fast path for runtimes that can generate code, and the only implementation the library had
- /// before it grew a Native AOT counterpart. It is reached exclusively through the
- /// check in ;
- /// is the counterpart that produces the same results
- /// without generating code.
+ /// Callers must have established that the type has a parameterless constructor
+ /// and no constructor compatible with the result set - does both. Fields
+ /// without a matching property are never read, which the compiled expression tree does as well.
///
- [RequiresDynamicCode(MaterializerRequiresDynamicCodeMessage)]
- private static Delegate CreateExpressionMaterializer<
+ private static Func CreateReflectionPropertyMaterializer<
[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity
- >(
- DbDataReader dataReader,
- String[] dataReaderFieldNames,
- Type[] dataReaderFieldTypes
- )
+ >(DbDataReader dataReader, string[] dataReaderFieldNames, Type[] dataReaderFieldTypes)
{
var entityType = typeof(TEntity);
- /*
- * This method creates an expression tree to generate a materializer function instead of using reflection for
- * the materialization, because using reflection would be significantly slower.
- * Using expression trees also allows us to use the typed GetXXX methods of DbDataReader, which avoids boxing
- * in many cases.
- */
-
- var dataReaderParameterExpression = Expression.Parameter(typeof(DbDataReader), "dataReader");
- var dataReaderFieldValueExpressions = new Expression[dataReader.FieldCount];
-
- var fieldOrdinalToTargetType = new Dictionary(dataReader.FieldCount);
- var fieldOrdinalToConstructorParameterIndex = new Dictionary(dataReader.FieldCount);
-
- var compatibleConstructor = EntityHelper.FindCompatibleConstructor(
- entityType,
- [.. dataReaderFieldNames.Zip(dataReaderFieldTypes, (name, type) => (name, type))]
- );
-
- var entityPropertiesByColumnName = EntityHelper.GetEntityTypeMetadata(entityType)
+ var entityPropertiesByColumnName = EntityHelper
+ .GetEntityTypeMetadata(entityType)
.MappedProperties.Where(a => a.CanWrite)
.ToDictionary(a => a.ColumnName, StringComparer.OrdinalIgnoreCase);
- if (compatibleConstructor is not null)
- {
- var constructorParameters = compatibleConstructor.GetParameters().ToList();
-
- for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
- {
- var constructorParameter = constructorParameters.First(p =>
- !String.IsNullOrWhiteSpace(p.Name) &&
- p.Name.Equals(dataReaderFieldNames[fieldOrdinal], StringComparison.OrdinalIgnoreCase) &&
- ValueConverter.CanConvert(dataReaderFieldTypes[fieldOrdinal], p.ParameterType)
- );
-
- fieldOrdinalToConstructorParameterIndex.Add(
- fieldOrdinal,
- constructorParameters.IndexOf(constructorParameter)
- );
-
- fieldOrdinalToTargetType.Add(fieldOrdinal, constructorParameter.ParameterType);
- }
- }
- else
- {
- for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
- {
- var dataReaderFieldName = dataReaderFieldNames[fieldOrdinal];
+ var entityConstructor = ConstructorInvoker.Create(EntityHelper.FindParameterlessConstructor(entityType)!);
- if (entityPropertiesByColumnName.TryGetValue(dataReaderFieldName, out var entityProperty))
- {
- fieldOrdinalToTargetType.Add(
- fieldOrdinal,
- entityProperty.PropertyType
- );
- }
- else
- {
- fieldOrdinalToTargetType.Add(
- fieldOrdinal,
- dataReaderFieldTypes[fieldOrdinal]
- );
- }
- }
- }
+ var propertyBindings = new List(dataReader.FieldCount);
for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
{
- var fieldOrdinalExpression = Expression.Constant(fieldOrdinal);
-
var dataReaderFieldName = dataReaderFieldNames[fieldOrdinal];
- if (compatibleConstructor is null && !entityPropertiesByColumnName.ContainsKey(dataReaderFieldName))
+ if (!entityPropertiesByColumnName.TryGetValue(dataReaderFieldName, out var entityProperty))
{
- // No need to read the field when we are using properties to materialize and there is no matching
- // property for the field.
+ // No need to read the field when there is no matching property for it. The expression tree skips
+ // these fields as well, and tests assert that the field is never touched.
continue;
}
var dataReaderFieldType = dataReaderFieldTypes[fieldOrdinal];
- var targetType = fieldOrdinalToTargetType[fieldOrdinal];
-
- // Basically:
- // dataReader.GetXXX(fieldOrdinal)
- var getFieldValueCallExpression = MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueExpression(
- dataReaderParameterExpression,
- fieldOrdinalExpression,
- fieldOrdinal,
- dataReaderFieldName,
- dataReaderFieldType
- );
-
- /*
- * Basically:
- *
- * if (dataReader.IsDBNull(fieldOrdinal))
- * {
- * if (targetType.IsReferenceTypeOrNullableType())
- * {
- * default(targetType);
- * }
- * else
- * {
- * throw new InvalidCastException(...);
- * }
- * }
- * else
- * {
- * if (dataReaderFieldType != targetType)
- * {
- * try
- * {
- * ValueConverter.ConvertValueToType((Object) dataReader.GetXXX(fieldOrdinal));
- * }
- * catch (Exception ex)
- * {
- * throw new InvalidCastException(..., ex);
- * }
- * }
- * else
- * {
- * dataReader.GetXXX(fieldOrdinal);
- * }
- * }
- */
-
- var exceptionParameterExpression = Expression.Parameter(typeof(Exception));
-
- Expression isDbNullBranchExpression = targetType.IsReferenceTypeOrNullableType()
- ? Expression.Default(targetType)
- : Expression.Throw(
- Expression.New(
- typeof(InvalidCastException).GetConstructor([typeof(String)])!,
- Expression.Constant(
- $"The column '{dataReaderFieldName}' returned by the SQL statement contains a " +
- $"NULL value, but the corresponding property of the type {entityType} is " +
- "non-nullable."
- )
- ),
- targetType
- );
-
- var throwInvalidCastExceptionExpression = Expression.Throw(
- Expression.New(
- typeof(InvalidCastException).GetConstructor(
- [typeof(String), typeof(Exception)]
- )!,
- Expression.Constant(
- $"The column '{dataReaderFieldName}' returned by the SQL statement " +
- $"contains a value that could not be converted to the type {targetType} " +
- $"of the corresponding property of the type {entityType}. See inner " +
- "exception for details."
- ),
- exceptionParameterExpression
- ),
- targetType
- );
+ var targetType = entityProperty.PropertyType;
- var convertFieldValueExpression = Expression.TryCatch(
- Expression.Convert(
- Expression.Call(
- null,
- MaterializerFactoryHelper.MakeValueConverterConvertValueToTypeMethod(targetType),
- Expression.Convert(getFieldValueCallExpression, typeof(Object))
+ propertyBindings.Add(
+ new ReflectionPropertyBinding(
+ new ReflectionColumnBinding(
+ dataReaderFieldName,
+ fieldOrdinal,
+ MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction(
+ fieldOrdinal,
+ dataReaderFieldName,
+ dataReaderFieldType
+ ),
+ dataReaderFieldType != targetType,
+ targetType
),
- targetType
- ),
- Expression.Catch(
- exceptionParameterExpression,
- throwInvalidCastExceptionExpression
+ // entityPropertiesByColumnName only contains writable properties, so the setter always exists.
+ entityProperty.PropertySetter!
)
);
-
- var isNotDbNullBranchExpression = dataReaderFieldType != targetType
- ? convertFieldValueExpression
- : getFieldValueCallExpression;
-
- dataReaderFieldValueExpressions[fieldOrdinal] =
- Expression.Condition(
- Expression.Call(
- dataReaderParameterExpression,
- MaterializerFactoryHelper.DbDataReaderIsDBNullMethod,
- fieldOrdinalExpression
- ),
- isDbNullBranchExpression,
- isNotDbNullBranchExpression
- );
}
- Expression bodyExpression;
-
- if (compatibleConstructor is not null)
- {
- var constructorArgumentExpressions = new Expression[dataReader.FieldCount];
-
- for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
- {
- var constructorArgumentIndex = fieldOrdinalToConstructorParameterIndex[fieldOrdinal];
+ var resolvedPropertyBindings = propertyBindings.ToArray();
- constructorArgumentExpressions[constructorArgumentIndex] =
- dataReaderFieldValueExpressions[fieldOrdinal];
- }
+ return rowDataReader =>
+ MaterializeEntityThroughProperties(
+ rowDataReader,
+ entityType,
+ entityConstructor,
+ resolvedPropertyBindings
+ );
+ }
- // Basically:
- // new TEntity(constructorArgumentExpressions...)
- bodyExpression = Expression.New(compatibleConstructor, constructorArgumentExpressions);
- }
- else
+ ///
+ /// Throws if none of the fields of the result set can be mapped to a writable property of
+ /// .
+ ///
+ /// The entity type the result set is being materialized to.
+ /// The field names of the result set.
+ /// The writable properties of the entity type, by column name.
+ ///
+ /// The result set has fields, but none of them maps to a writable property of .
+ ///
+ ///
+ ///
+ /// Without this check, materialization silently succeeds and returns entities whose properties are all left at
+ /// their default values, which is indistinguishable from a query that legitimately returned default data.
+ ///
+ ///
+ /// The check matters most in an application that is trimmed or published with Native AOT. If a
+ /// annotation is missing anywhere on the call path, the
+ /// trimmer removes the entity's properties, reflection then reports fewer members than the type really has, and
+ /// every column silently fails to bind. Failing loudly here is the backstop for that.
+ ///
+ ///
+ private static void GuardAgainstResultSetBindingNoProperties(
+ Type entityType,
+ string[] dataReaderFieldNames,
+ Dictionary entityPropertiesByColumnName
+ )
+ {
+ if (dataReaderFieldNames.Length == 0)
{
- var memberBindings = new List();
-
- for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
- {
- var dataReaderFieldName = dataReaderFieldNames[fieldOrdinal];
-
- if (!entityPropertiesByColumnName.TryGetValue(dataReaderFieldName, out var entityProperty))
- {
- continue;
- }
-
- memberBindings.Add(
- Expression.Bind(entityProperty.PropertyInfo, dataReaderFieldValueExpressions[fieldOrdinal])
- );
- }
+ return;
+ }
- // Basically:
- // new TEntity { Property1 = ..., Property2 = ..., ... }
- bodyExpression = Expression.MemberInit(Expression.New(entityType), memberBindings);
+ if (dataReaderFieldNames.Any(entityPropertiesByColumnName.ContainsKey))
+ {
+ return;
}
- return Expression.Lambda(bodyExpression, dataReaderParameterExpression).Compile();
+ throw new InvalidOperationException(
+ $"None of the {dataReaderFieldNames.Length} field(s) of the result set "
+ + $"({string.Join(", ", dataReaderFieldNames)}) could be mapped to a writable property of the entity "
+ + $"type {entityType}. Materializing the result set would return entities whose properties are all left "
+ + "at their default values. Check that the field names of the result set match the property names, or "
+ + "the mapped column names, of the entity type. If the application is trimmed or published with Native "
+ + "AOT, this usually means the properties of the entity type were removed by the trimmer because a "
+ + "[DynamicallyAccessedMembers] annotation is missing on the call path."
+ );
}
///
@@ -827,12 +799,15 @@ private static TEntity MaterializeEntityThroughConstructor(
ReflectionColumnBinding[] constructorArgumentBindings
)
{
- var constructorArguments = new Object?[constructorArgumentBindings.Length];
+ var constructorArguments = new object?[constructorArgumentBindings.Length];
for (var argumentIndex = 0; argumentIndex < constructorArgumentBindings.Length; argumentIndex++)
{
- constructorArguments[argumentIndex] =
- ReadFieldValue(dataReader, entityType, constructorArgumentBindings[argumentIndex]);
+ constructorArguments[argumentIndex] = ReadFieldValue(
+ dataReader,
+ entityType,
+ constructorArgumentBindings[argumentIndex]
+ );
}
return (TEntity)entityConstructor.Invoke(constructorArguments.AsSpan());
@@ -890,7 +865,7 @@ ReflectionPropertyBinding[] propertyBindings
///
///
///
- private static Object? ReadFieldValue(
+ private static object? ReadFieldValue(
DbDataReader dataReader,
Type entityType,
ReflectionColumnBinding columnBinding
@@ -904,8 +879,8 @@ ReflectionColumnBinding columnBinding
}
throw new InvalidCastException(
- $"The column '{columnBinding.FieldName}' returned by the SQL statement contains a NULL value, but " +
- $"the corresponding property of the type {entityType} is non-nullable."
+ $"The column '{columnBinding.FieldName}' returned by the SQL statement contains a NULL value, but "
+ + $"the corresponding property of the type {entityType} is non-nullable."
);
}
@@ -923,9 +898,9 @@ ReflectionColumnBinding columnBinding
catch (Exception exception)
{
throw new InvalidCastException(
- $"The column '{columnBinding.FieldName}' returned by the SQL statement contains a value that could " +
- $"not be converted to the type {columnBinding.TargetType} of the corresponding property of the " +
- $"type {entityType}. See inner exception for details.",
+ $"The column '{columnBinding.FieldName}' returned by the SQL statement contains a value that could "
+ + $"not be converted to the type {columnBinding.TargetType} of the corresponding property of the "
+ + $"type {entityType}. See inner exception for details.",
exception
);
}
@@ -973,7 +948,7 @@ ReflectionColumnBinding columnBinding
private static void ValidateDataReader(
[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] Type entityType,
DbDataReader dataReader,
- String[] dataReaderFieldNames,
+ string[] dataReaderFieldNames,
Type[] dataReaderFieldTypes
)
{
@@ -987,11 +962,11 @@ Type[] dataReaderFieldTypes
var dataReaderFieldName = dataReaderFieldNames[fieldOrdinal];
var dataReaderFieldType = dataReaderFieldTypes[fieldOrdinal];
- if (String.IsNullOrWhiteSpace(dataReaderFieldName))
+ if (string.IsNullOrWhiteSpace(dataReaderFieldName))
{
throw new ArgumentException(
- $"The {(fieldOrdinal + 1).OrdinalizeEnglish()} column returned by the SQL statement does not " +
- "have a name. Make sure that all columns the statement returns have a name.",
+ $"The {(fieldOrdinal + 1).OrdinalizeEnglish()} column returned by the SQL statement does not "
+ + "have a name. Make sure that all columns the statement returns have a name.",
nameof(dataReader)
);
}
@@ -999,8 +974,8 @@ Type[] dataReaderFieldTypes
if (!MaterializerFactoryHelper.IsDbDataReaderTypedGetMethodAvailable(dataReaderFieldType))
{
throw new ArgumentException(
- $"The data type {dataReaderFieldType} of the column '{dataReaderFieldName}' returned by the " +
- "SQL statement is not supported.",
+ $"The data type {dataReaderFieldType} of the column '{dataReaderFieldName}' returned by the "
+ + "SQL statement is not supported.",
nameof(dataReader)
);
}
@@ -1023,23 +998,24 @@ [.. dataReaderFieldNames.Zip(dataReaderFieldTypes, (name, type) => (name, type))
if (parameterlessConstructor is null)
{
var exampleConstructorSignature =
- "(" +
- String.Join(
+ "("
+ + string.Join(
", ",
dataReaderFieldNames.Zip(dataReaderFieldTypes, (name, type) => $"{type.Name} {name}")
- ) +
- ")";
+ )
+ + ")";
throw new ArgumentException(
- $"Could not materialize an instance of the type {entityType}. The type either needs to have a " +
- "parameterless constructor or a constructor whose parameters match the columns returned by the SQL " +
- $"statement, e.g. a constructor that has the following signature:{Environment.NewLine}" +
- $"{exampleConstructorSignature}.",
+ $"Could not materialize an instance of the type {entityType}. The type either needs to have a "
+ + "parameterless constructor or a constructor whose parameters match the columns returned by the SQL "
+ + $"statement, e.g. a constructor that has the following signature:{Environment.NewLine}"
+ + $"{exampleConstructorSignature}.",
nameof(entityType)
);
}
- var entityPropertiesByColumnName = EntityHelper.GetEntityTypeMetadata(entityType)
+ var entityPropertiesByColumnName = EntityHelper
+ .GetEntityTypeMetadata(entityType)
.MappedProperties.Where(a => a.CanWrite)
.ToDictionary(a => a.ColumnName, StringComparer.OrdinalIgnoreCase);
@@ -1058,17 +1034,15 @@ [.. dataReaderFieldNames.Zip(dataReaderFieldTypes, (name, type) => (name, type))
if (!ValueConverter.CanConvert(dataReaderFieldType, entityPropertyType))
{
throw new ArgumentException(
- $"The data type {dataReaderFieldType} of the column '{dataReaderFieldName}' returned by the " +
- $"SQL statement is not compatible with the property type {entityPropertyType} of the " +
- $"corresponding property of the type {entityType}.",
+ $"The data type {dataReaderFieldType} of the column '{dataReaderFieldName}' returned by the "
+ + $"SQL statement is not compatible with the property type {entityPropertyType} of the "
+ + $"corresponding property of the type {entityType}.",
nameof(dataReader)
);
}
}
}
- private static readonly ConcurrentDictionary materializerCache = [];
-
///
/// A cache key used to uniquely identify an entity materializer.
///
@@ -1083,28 +1057,29 @@ [.. dataReaderFieldNames.Zip(dataReaderFieldTypes, (name, type) => (name, type))
///
private readonly struct MaterializerCacheKey(
Type entityType,
- String[] dataReaderFieldNames,
+ string[] dataReaderFieldNames,
Type[] dataReaderFieldTypes
- )
- : IEquatable
+ ) : IEquatable
{
///
/// The type of entity the materializer materializes.
///
public Type EntityType { get; } = entityType;
+ private string[] DataReaderFieldNames { get; } = dataReaderFieldNames;
+ private Type[] DataReaderFieldTypes { get; } = dataReaderFieldTypes;
+
///
- public Boolean Equals(MaterializerCacheKey other) =>
- this.EntityType == other.EntityType &&
- this.DataReaderFieldNames.SequenceEqual(other.DataReaderFieldNames) &&
- this.DataReaderFieldTypes.SequenceEqual(other.DataReaderFieldTypes);
+ public bool Equals(MaterializerCacheKey other) =>
+ this.EntityType == other.EntityType
+ && this.DataReaderFieldNames.SequenceEqual(other.DataReaderFieldNames)
+ && this.DataReaderFieldTypes.SequenceEqual(other.DataReaderFieldTypes);
///
- public override Boolean Equals(Object? obj) =>
- obj is MaterializerCacheKey other && this.Equals(other);
+ public override bool Equals(object? obj) => obj is MaterializerCacheKey other && this.Equals(other);
///
- public override Int32 GetHashCode()
+ public override int GetHashCode()
{
var hashCode = new HashCode();
@@ -1122,9 +1097,6 @@ public override Int32 GetHashCode()
return hashCode.ToHashCode();
}
-
- private String[] DataReaderFieldNames { get; } = dataReaderFieldNames;
- private Type[] DataReaderFieldTypes { get; } = dataReaderFieldTypes;
}
///
@@ -1146,10 +1118,10 @@ public override Int32 GetHashCode()
/// constructor parameter it is passed to.
///
private readonly record struct ReflectionColumnBinding(
- String FieldName,
- Int32 FieldOrdinal,
- Func GetFieldValue,
- Boolean NeedsConversion,
+ string FieldName,
+ int FieldOrdinal,
+ Func GetFieldValue,
+ bool NeedsConversion,
Type TargetType
);
@@ -1163,6 +1135,6 @@ Type TargetType
///
private readonly record struct ReflectionPropertyBinding(
ReflectionColumnBinding Column,
- Action PropertySetter
+ Action PropertySetter
);
}
diff --git a/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs b/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs
index a188296..827eec9 100644
--- a/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs
+++ b/src/DbConnectionPlus/Materializers/MaterializerFactoryHelper.cs
@@ -13,81 +13,97 @@ namespace RentADeveloper.DbConnectionPlus.Materializers;
///
internal static class MaterializerFactoryHelper
{
+ private static readonly Dictionary dbDataReaderTypedGetMethods = new()
+ {
+ { typeof(bool), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetBoolean))! },
+ { typeof(byte), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetByte))! },
+ { typeof(DateTime), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetDateTime))! },
+ { typeof(decimal), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetDecimal))! },
+ { typeof(double), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetDouble))! },
+ { typeof(float), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetFloat))! },
+ { typeof(Guid), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetGuid))! },
+ { typeof(short), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetInt16))! },
+ { typeof(int), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetInt32))! },
+ { typeof(long), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetInt64))! },
+ { typeof(string), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetString))! },
+ };
+
///
- /// The method.
+ /// The functions that read a field value using the same typed .GetXXX method as
+ /// , for the materializer path that cannot compile an expression tree.
///
- internal static MethodInfo DbDataReaderGetValueMethod { get; } = typeof(DbDataReader)
- .GetMethod(nameof(DbDataReader.GetValue))!;
+ ///
+ /// The key set must stay identical to the key set of , otherwise the
+ /// two materializer paths disagree on which field types are supported.
+ ///
+ private static readonly Dictionary> dbDataReaderTypedGetValueFunctions =
+ new()
+ {
+ { typeof(bool), static (dataReader, fieldOrdinal) => dataReader.GetBoolean(fieldOrdinal) },
+ { typeof(byte), static (dataReader, fieldOrdinal) => dataReader.GetByte(fieldOrdinal) },
+ { typeof(DateTime), static (dataReader, fieldOrdinal) => dataReader.GetDateTime(fieldOrdinal) },
+ { typeof(decimal), static (dataReader, fieldOrdinal) => dataReader.GetDecimal(fieldOrdinal) },
+ { typeof(double), static (dataReader, fieldOrdinal) => dataReader.GetDouble(fieldOrdinal) },
+ { typeof(float), static (dataReader, fieldOrdinal) => dataReader.GetFloat(fieldOrdinal) },
+ { typeof(Guid), static (dataReader, fieldOrdinal) => dataReader.GetGuid(fieldOrdinal) },
+ { typeof(short), static (dataReader, fieldOrdinal) => dataReader.GetInt16(fieldOrdinal) },
+ { typeof(int), static (dataReader, fieldOrdinal) => dataReader.GetInt32(fieldOrdinal) },
+ { typeof(long), static (dataReader, fieldOrdinal) => dataReader.GetInt64(fieldOrdinal) },
+ { typeof(string), static (dataReader, fieldOrdinal) => dataReader.GetString(fieldOrdinal) },
+ };
///
- /// The method.
+ /// The field types has no typed GetXXX method for, and which
+ /// therefore reads through
+ /// instead.
///
- // ReSharper disable once InconsistentNaming
- internal static MethodInfo DbDataReaderIsDBNullMethod { get; } = typeof(DbDataReader)
- .GetMethod(nameof(DbDataReader.IsDBNull))!;
+ private static readonly HashSet dbDataReaderUntypedFieldTypes =
+ [
+ typeof(byte[]),
+ typeof(DateOnly),
+ typeof(DateTimeOffset),
+ typeof(TimeOnly),
+ typeof(TimeSpan),
+ ];
///
- /// The 'Chars' property of the type.
+ /// The generic method definition of the method, cached
+ /// for .
///
- internal static PropertyInfo StringCharsProperty { get; } = typeof(String)
- .GetProperty("Chars", BindingFlags.Instance | BindingFlags.Public)!;
+ private static readonly MethodInfo valueConverterConvertValueToTypeMethod = typeof(ValueConverter)
+ .GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
+ .First(m => m is { Name: nameof(ValueConverter.ConvertValueToType), IsGenericMethod: true });
///
- /// The method.
+ /// The method.
///
- internal static MethodInfo StringConcatMethod { get; } = typeof(String)
- .GetMethod(nameof(String.Concat), [typeof(String), typeof(String), typeof(String)])!;
+ internal static MethodInfo DbDataReaderGetValueMethod { get; } =
+ typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetValue))!;
///
- /// The property.
+ /// The method.
///
- internal static PropertyInfo StringLengthProperty { get; } = typeof(String)
- .GetProperty(nameof(String.Length), BindingFlags.Instance | BindingFlags.Public)!;
+ // ReSharper disable once InconsistentNaming
+ internal static MethodInfo DbDataReaderIsDBNullMethod { get; } =
+ typeof(DbDataReader).GetMethod(nameof(DbDataReader.IsDBNull))!;
///
- /// Specializes over , so that
- /// a compiled expression tree can call the generic
- /// directly.
+ /// The 'Chars' property of the type.
///
- /// The type to specialize the method over.
- ///
- /// The method, specialized over
- /// .
- ///
- ///
- ///
- /// This exists as its own method purely so that the IL2060 suppression below covers one line of code
- /// instead of the whole expression-building method it is called from. Both materializer factories call it.
- ///
- ///
- /// The suppression is sound rather than convenient, and provably so:
- /// declares no
- /// on its type parameter. IL2060 reports that the
- /// requirements of a runtime-specialized generic method cannot be guaranteed; here there are no requirements to
- /// guarantee, so there is nothing the trimmer could remove and nothing for a consumer to act on. Whether the
- /// conversion reflects over the target type at all is a question about the converters, which are annotation-free
- /// and verified so: the trim analyzers report nothing for ValueConverter or EnumConverter.
- ///
- ///
- /// is a different matter and is kept: specializing a generic method
- /// over a value type at run time genuinely needs code generation. Only the callers that are guarded by
- /// may reach this.
- ///
- ///
- [RequiresDynamicCode(
- "Specializing a generic method over a value type at run time is not supported when the application is " +
- "published with Native AOT. Call this only from a RuntimeFeature.IsDynamicCodeSupported branch."
- )]
- [UnconditionalSuppressMessage(
- "Trimming",
- "IL2060:MakeGenericMethod call cannot be statically analyzed",
- Justification =
- "ValueConverter.ConvertValueToType declares no DynamicallyAccessedMembers on TTarget, so the " +
- "specialized instantiation has no requirements that trimming could fail to preserve. Reaching this " +
- "method at all requires a RuntimeFeature.IsDynamicCodeSupported branch."
- )]
- internal static MethodInfo MakeValueConverterConvertValueToTypeMethod(Type targetType) =>
- valueConverterConvertValueToTypeMethod.MakeGenericMethod(targetType);
+ internal static PropertyInfo StringCharsProperty { get; } =
+ typeof(string).GetProperty("Chars", BindingFlags.Instance | BindingFlags.Public)!;
+
+ ///
+ /// The method.
+ ///
+ internal static MethodInfo StringConcatMethod { get; } =
+ typeof(string).GetMethod(nameof(string.Concat), [typeof(string), typeof(string), typeof(string)])!;
+
+ ///
+ /// The property.
+ ///
+ internal static PropertyInfo StringLengthProperty { get; } =
+ typeof(string).GetProperty(nameof(string.Length), BindingFlags.Instance | BindingFlags.Public)!;
///
/// Creates an that gets the value of a field of the specified field type from a
@@ -128,8 +144,8 @@ internal static MethodInfo MakeValueConverterConvertValueToTypeMethod(Type targe
internal static Expression CreateGetDbDataReaderFieldValueExpression(
Expression dataReaderExpression,
Expression fieldOrdinalExpression,
- Int32 fieldOrdinal,
- String? fieldName,
+ int fieldOrdinal,
+ string? fieldName,
Type fieldType
)
{
@@ -137,104 +153,75 @@ Type fieldType
ArgumentNullException.ThrowIfNull(fieldOrdinalExpression);
ArgumentNullException.ThrowIfNull(fieldType);
- if (fieldType == typeof(Byte[]))
+ if (fieldType == typeof(byte[]))
{
// Special handling for byte arrays since DbDataReader does not have a GetBytes method that returns
// a byte array directly.
- return
- Expression.Convert(
- Expression.Call(
- dataReaderExpression,
- DbDataReaderGetValueMethod,
- fieldOrdinalExpression
- ),
- typeof(Byte[])
- );
+ return Expression.Convert(
+ Expression.Call(dataReaderExpression, DbDataReaderGetValueMethod, fieldOrdinalExpression),
+ typeof(byte[])
+ );
}
if (fieldType == typeof(TimeSpan))
{
// Special handling for the type TimeSpan since DbDataReader does not have a GetTimeSpan method that
// returns a TimeSpan directly.
- return
- Expression.Convert(
- Expression.Call(
- dataReaderExpression,
- DbDataReaderGetValueMethod,
- fieldOrdinalExpression
- ),
- typeof(TimeSpan)
- );
+ return Expression.Convert(
+ Expression.Call(dataReaderExpression, DbDataReaderGetValueMethod, fieldOrdinalExpression),
+ typeof(TimeSpan)
+ );
}
if (fieldType == typeof(TimeOnly))
{
// Special handling for the type TimeOnly since DbDataReader does not have a GetTimeOnly method that
// returns a TimeOnly directly.
- return
- Expression.Convert(
- Expression.Call(
- dataReaderExpression,
- DbDataReaderGetValueMethod,
- fieldOrdinalExpression
- ),
- typeof(TimeOnly)
- );
+ return Expression.Convert(
+ Expression.Call(dataReaderExpression, DbDataReaderGetValueMethod, fieldOrdinalExpression),
+ typeof(TimeOnly)
+ );
}
if (fieldType == typeof(DateOnly))
{
// Special handling for the type DateOnly since DbDataReader does not have a GetDateOnly method
// that returns a DateOnly directly.
- return
- Expression.Convert(
- Expression.Call(
- dataReaderExpression,
- DbDataReaderGetValueMethod,
- fieldOrdinalExpression
- ),
- typeof(DateOnly)
- );
+ return Expression.Convert(
+ Expression.Call(dataReaderExpression, DbDataReaderGetValueMethod, fieldOrdinalExpression),
+ typeof(DateOnly)
+ );
}
if (fieldType == typeof(DateTimeOffset))
{
// Special handling for the type DateTimeOffset since DbDataReader does not have a GetDateTimeOffset method
// that returns a DateTimeOffset directly.
- return
- Expression.Convert(
- Expression.Call(
- dataReaderExpression,
- DbDataReaderGetValueMethod,
- fieldOrdinalExpression
- ),
- typeof(DateTimeOffset)
- );
+ return Expression.Convert(
+ Expression.Call(dataReaderExpression, DbDataReaderGetValueMethod, fieldOrdinalExpression),
+ typeof(DateTimeOffset)
+ );
}
if (!dbDataReaderTypedGetMethods.TryGetValue(fieldType, out var dbDataReaderGetMethod))
{
- if (!String.IsNullOrWhiteSpace(fieldName))
+ if (!string.IsNullOrWhiteSpace(fieldName))
{
throw new ArgumentException(
- $"The data type {fieldType} of the column '{fieldName}' returned by the SQL statement is not " +
- "supported.",
+ $"The data type {fieldType} of the column '{fieldName}' returned by the SQL statement is not "
+ + "supported.",
nameof(fieldType)
);
}
throw new ArgumentException(
- $"The data type {fieldType} of the {(fieldOrdinal + 1).OrdinalizeEnglish()} column returned by the " +
- "SQL statement is not supported.",
+ $"The data type {fieldType} of the {(fieldOrdinal + 1).OrdinalizeEnglish()} column returned by the "
+ + "SQL statement is not supported.",
nameof(fieldType)
);
}
- return Expression.Call(
- dataReaderExpression,
- dbDataReaderGetMethod,
- fieldOrdinalExpression
- );
+ return Expression.Call(dataReaderExpression, dbDataReaderGetMethod, fieldOrdinalExpression);
}
///
@@ -246,7 +233,7 @@ Type fieldType
/// The field ordinal of the field to get the value from.
/// The field name of the field to get the value from.
/// The field type of the field to get the value from.
- /// The created function. It returns the field value boxed in an .
+ /// The created function. It returns the field value boxed in an .
///
/// The specified type is not supported.
///
@@ -262,9 +249,9 @@ Type fieldType
/// counterpart, so picking a different method here would make the two materializer paths disagree.
///
///
- internal static Func CreateGetDbDataReaderFieldValueFunction(
- Int32 fieldOrdinal,
- String? fieldName,
+ internal static Func CreateGetDbDataReaderFieldValueFunction(
+ int fieldOrdinal,
+ string? fieldName,
Type fieldType
)
{
@@ -279,18 +266,18 @@ Type fieldType
if (!dbDataReaderTypedGetValueFunctions.TryGetValue(fieldType, out var dbDataReaderGetValueFunction))
{
- if (!String.IsNullOrWhiteSpace(fieldName))
+ if (!string.IsNullOrWhiteSpace(fieldName))
{
throw new ArgumentException(
- $"The data type {fieldType} of the column '{fieldName}' returned by the SQL statement is not " +
- "supported.",
+ $"The data type {fieldType} of the column '{fieldName}' returned by the SQL statement is not "
+ + "supported.",
nameof(fieldType)
);
}
throw new ArgumentException(
- $"The data type {fieldType} of the {(fieldOrdinal + 1).OrdinalizeEnglish()} column returned by the " +
- "SQL statement is not supported.",
+ $"The data type {fieldType} of the {(fieldOrdinal + 1).OrdinalizeEnglish()} column returned by the "
+ + "SQL statement is not supported.",
nameof(fieldType)
);
}
@@ -308,72 +295,54 @@ Type fieldType
/// ; otherwise, .
///
/// is .
- internal static Boolean IsDbDataReaderTypedGetMethodAvailable(Type fieldType)
+ internal static bool IsDbDataReaderTypedGetMethodAvailable(Type fieldType)
{
ArgumentNullException.ThrowIfNull(fieldType);
- return dbDataReaderTypedGetMethods.ContainsKey(fieldType) ||
- dbDataReaderUntypedFieldTypes.Contains(fieldType);
+ return dbDataReaderTypedGetMethods.ContainsKey(fieldType) || dbDataReaderUntypedFieldTypes.Contains(fieldType);
}
- private static readonly Dictionary dbDataReaderTypedGetMethods = new()
- {
- { typeof(Boolean), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetBoolean))! },
- { typeof(Byte), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetByte))! },
- { typeof(DateTime), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetDateTime))! },
- { typeof(Decimal), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetDecimal))! },
- { typeof(Double), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetDouble))! },
- { typeof(Single), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetFloat))! },
- { typeof(Guid), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetGuid))! },
- { typeof(Int16), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetInt16))! },
- { typeof(Int32), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetInt32))! },
- { typeof(Int64), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetInt64))! },
- { typeof(String), typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetString))! }
- };
-
///
- /// The functions that read a field value using the same typed .GetXXX method as
- /// , for the materializer path that cannot compile an expression tree.
+ /// Specializes over , so that
+ /// a compiled expression tree can call the generic
+ /// directly.
///
+ /// The type to specialize the method over.
+ ///
+ /// The method, specialized over
+ /// .
+ ///
///
- /// The key set must stay identical to the key set of , otherwise the
- /// two materializer paths disagree on which field types are supported.
+ ///
+ /// This exists as its own method purely so that the IL2060 suppression below covers one line of code
+ /// instead of the whole expression-building method it is called from. Both materializer factories call it.
+ ///
+ ///
+ /// The suppression is sound rather than convenient, and provably so:
+ /// declares no
+ /// on its type parameter. IL2060 reports that the
+ /// requirements of a runtime-specialized generic method cannot be guaranteed; here there are no requirements to
+ /// guarantee, so there is nothing the trimmer could remove and nothing for a consumer to act on. Whether the
+ /// conversion reflects over the target type at all is a question about the converters, which are annotation-free
+ /// and verified so: the trim analyzers report nothing for ValueConverter or EnumConverter.
+ ///
+ ///
+ /// is a different matter and is kept: specializing a generic method
+ /// over a value type at run time genuinely needs code generation. Only the callers that are guarded by
+ /// may reach this.
+ ///
///
- private static readonly Dictionary> dbDataReaderTypedGetValueFunctions =
- new()
- {
- { typeof(Boolean), static (dataReader, fieldOrdinal) => dataReader.GetBoolean(fieldOrdinal) },
- { typeof(Byte), static (dataReader, fieldOrdinal) => dataReader.GetByte(fieldOrdinal) },
- { typeof(DateTime), static (dataReader, fieldOrdinal) => dataReader.GetDateTime(fieldOrdinal) },
- { typeof(Decimal), static (dataReader, fieldOrdinal) => dataReader.GetDecimal(fieldOrdinal) },
- { typeof(Double), static (dataReader, fieldOrdinal) => dataReader.GetDouble(fieldOrdinal) },
- { typeof(Single), static (dataReader, fieldOrdinal) => dataReader.GetFloat(fieldOrdinal) },
- { typeof(Guid), static (dataReader, fieldOrdinal) => dataReader.GetGuid(fieldOrdinal) },
- { typeof(Int16), static (dataReader, fieldOrdinal) => dataReader.GetInt16(fieldOrdinal) },
- { typeof(Int32), static (dataReader, fieldOrdinal) => dataReader.GetInt32(fieldOrdinal) },
- { typeof(Int64), static (dataReader, fieldOrdinal) => dataReader.GetInt64(fieldOrdinal) },
- { typeof(String), static (dataReader, fieldOrdinal) => dataReader.GetString(fieldOrdinal) }
- };
-
- ///
- /// The field types has no typed GetXXX method for, and which
- /// therefore reads through
- /// instead.
- ///
- private static readonly HashSet dbDataReaderUntypedFieldTypes =
- [
- typeof(Byte[]),
- typeof(DateOnly),
- typeof(DateTimeOffset),
- typeof(TimeOnly),
- typeof(TimeSpan)
- ];
-
- ///
- /// The generic method definition of the method, cached
- /// for .
- ///
- private static readonly MethodInfo valueConverterConvertValueToTypeMethod = typeof(ValueConverter)
- .GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
- .First(m => m is { Name: nameof(ValueConverter.ConvertValueToType), IsGenericMethod: true });
+ [RequiresDynamicCode(
+ "Specializing a generic method over a value type at run time is not supported when the application is "
+ + "published with Native AOT. Call this only from a RuntimeFeature.IsDynamicCodeSupported branch."
+ )]
+ [UnconditionalSuppressMessage(
+ "Trimming",
+ "IL2060:MakeGenericMethod call cannot be statically analyzed",
+ Justification = "ValueConverter.ConvertValueToType declares no DynamicallyAccessedMembers on TTarget, so the "
+ + "specialized instantiation has no requirements that trimming could fail to preserve. Reaching this "
+ + "method at all requires a RuntimeFeature.IsDynamicCodeSupported branch."
+ )]
+ internal static MethodInfo MakeValueConverterConvertValueToTypeMethod(Type targetType) =>
+ valueConverterConvertValueToTypeMethod.MakeGenericMethod(targetType);
}
diff --git a/src/DbConnectionPlus/Materializers/ValueTupleMaterializerFactory.cs b/src/DbConnectionPlus/Materializers/ValueTupleMaterializerFactory.cs
index 169a857..c2e827f 100644
--- a/src/DbConnectionPlus/Materializers/ValueTupleMaterializerFactory.cs
+++ b/src/DbConnectionPlus/Materializers/ValueTupleMaterializerFactory.cs
@@ -25,10 +25,10 @@ internal static class ValueTupleMaterializerFactory
/// branch; the attribute exists so that the analyzer
/// verifies that guard rather than so that a warning propagates.
///
- internal const String MaterializerRequiresDynamicCodeMessage =
- "Materializing value tuples compiles an expression tree at run time, which is not supported when the " +
- "application is published with Native AOT. Reach this only from a RuntimeFeature.IsDynamicCodeSupported " +
- "branch.";
+ internal const string MaterializerRequiresDynamicCodeMessage =
+ "Materializing value tuples compiles an expression tree at run time, which is not supported when the "
+ + "application is published with Native AOT. Reach this only from a RuntimeFeature.IsDynamicCodeSupported "
+ + "branch.";
///
/// The members of a value tuple type that this library reflects over, and which therefore must survive trimming.
@@ -53,14 +53,115 @@ internal static class ValueTupleMaterializerFactory
///
///
internal const DynamicallyAccessedMemberTypes ValueTupleMemberTypes =
- DynamicallyAccessedMemberTypes.PublicFields |
- DynamicallyAccessedMemberTypes.PublicConstructors;
+ DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicConstructors;
///
/// The number of fields a value tuple holds before the runtime represents the remaining ones as a nested value
/// tuple in its Rest field.
///
- private const Int32 ValueTupleFieldCountBeforeNesting = 7;
+ private const int ValueTupleFieldCountBeforeNesting = 7;
+
+ private static readonly ConcurrentDictionary materializerCache = [];
+
+ ///
+ /// Creates a materializer function that materializes the data in a to an instance of
+ /// the value tuple type using reflection instead of a compiled expression
+ /// tree.
+ ///
+ /// The type of value tuple to materialize.
+ /// The for which to create the materializer function.
+ ///
+ /// The names of the fields in .
+ /// The order of the names must match the order of the fields in .
+ ///
+ ///
+ /// The field types of the fields in .
+ /// The order of the types must match the order of the fields in .
+ ///
+ ///
+ /// A function that materializes the data in a to an instance of the value tuple type
+ /// .
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// This is the materializer for applications published with Native AOT, where no run-time code generation is
+ /// available. Value tuples are materialized ordinal-positionally, exactly as
+ /// materializes them, including value tuples with more
+ /// than seven fields, which the runtime represents as nested value tuples.
+ ///
+ ///
+ /// Everything that depends only on the shape of the result set - the field ordinals, the target types, whether a
+ /// field value needs to be converted, and the constructors of the value tuple types - is resolved once, exactly
+ /// as the compiled expression tree bakes it in. Per row the materializer only walks an array, reads the fields
+ /// and passes them to the constructors. The materializer cache is keyed by that shape, so the resolution happens
+ /// once per shape.
+ ///
+ ///
+ /// The exception types and the exception messages are identical to the ones of the compiled expression tree, so
+ /// that the behaviour a consumer observes does not depend on how the application was published.
+ ///
+ ///
+ internal static Func CreateReflectionMaterializer<
+ [DynamicallyAccessedMembers(ValueTupleMemberTypes)] TValueTuple
+ >(DbDataReader dataReader, string[] dataReaderFieldNames, Type[] dataReaderFieldTypes)
+ {
+ ArgumentNullException.ThrowIfNull(dataReader);
+ ArgumentNullException.ThrowIfNull(dataReaderFieldNames);
+ ArgumentNullException.ThrowIfNull(dataReaderFieldTypes);
+
+ var valueTupleType = typeof(TValueTuple);
+
+ // Resolved here rather than taken from the caller, so that this method can be reached directly from a test:
+ // on the JIT the dispatch in CreateMaterializer always picks the expression tree.
+ var valueTupleFieldTypes = GetValueTupleFieldTypes(valueTupleType);
+
+ var columnBindings = new ReflectionColumnBinding[dataReader.FieldCount];
+
+ for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
+ {
+ var dataReaderFieldName = dataReaderFieldNames[fieldOrdinal];
+ var dataReaderFieldType = dataReaderFieldTypes[fieldOrdinal];
+ var targetType = valueTupleFieldTypes[fieldOrdinal];
+
+ columnBindings[fieldOrdinal] = new(
+ GetColumnNameOrPosition(fieldOrdinal, dataReaderFieldName),
+ fieldOrdinal,
+ MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction(
+ fieldOrdinal,
+ dataReaderFieldName,
+ dataReaderFieldType
+ ),
+ dataReaderFieldType != targetType,
+ targetType
+ );
+ }
+
+ var valueTupleConstructors = GetValueTupleConstructors(valueTupleType)
+ .Select(ConstructorInvoker.Create)
+ .ToArray();
+
+ return rowDataReader =>
+ MaterializeValueTuple(rowDataReader, valueTupleType, valueTupleConstructors, columnBindings);
+ }
///
/// Gets a materializer function that materializes the data in a to an instance of the
@@ -172,175 +273,53 @@ internal static Func GetMaterializer<
}
///
- /// Creates a materializer function that materializes the data in a to an instance of
- /// the value tuple type using reflection instead of a compiled expression
- /// tree.
+ /// Constructs the value tuple that holds .
///
- /// The type of value tuple to materialize.
- /// The for which to create the materializer function.
- ///
- /// The names of the fields in .
- /// The order of the names must match the order of the fields in .
+ ///
+ /// The constructors of the value tuple types, from the outermost to the innermost, as returned by
+ /// .
///
- ///
- /// The field types of the fields in .
- /// The order of the types must match the order of the fields in .
+ ///
+ /// The value of every field of the value tuple, including the fields of all nested value tuples, in the order in
+ /// which the fields are declared.
///
- ///
- /// A function that materializes the data in a to an instance of the value tuple type
- /// .
- ///
- ///
- ///
- ///
- ///
- /// is .
- ///
- ///
- ///
- ///
- /// is .
- ///
- ///
- ///
- ///
- /// is .
- ///
- ///
- ///
- ///
+ /// The constructed value tuple, boxed.
///
- ///
- /// This is the materializer for applications published with Native AOT, where no run-time code generation is
- /// available. Value tuples are materialized ordinal-positionally, exactly as
- /// materializes them, including value tuples with more
- /// than seven fields, which the runtime represents as nested value tuples.
- ///
- ///
- /// Everything that depends only on the shape of the result set - the field ordinals, the target types, whether a
- /// field value needs to be converted, and the constructors of the value tuple types - is resolved once, exactly
- /// as the compiled expression tree bakes it in. Per row the materializer only walks an array, reads the fields
- /// and passes them to the constructors. The materializer cache is keyed by that shape, so the resolution happens
- /// once per shape.
- ///
- ///
- /// The exception types and the exception messages are identical to the ones of the compiled expression tree, so
- /// that the behaviour a consumer observes does not depend on how the application was published.
- ///
+ /// This does the same as the tail of , which builds the
+ /// same nesting out of instead of constructing it.
///
- internal static Func CreateReflectionMaterializer<
- [DynamicallyAccessedMembers(ValueTupleMemberTypes)] TValueTuple
- >(
- DbDataReader dataReader,
- String[] dataReaderFieldNames,
- Type[] dataReaderFieldTypes
- )
+ private static object ConstructValueTuple(ConstructorInvoker[] valueTupleConstructors, object?[] fieldValues)
{
- ArgumentNullException.ThrowIfNull(dataReader);
- ArgumentNullException.ThrowIfNull(dataReaderFieldNames);
- ArgumentNullException.ThrowIfNull(dataReaderFieldTypes);
-
- var valueTupleType = typeof(TValueTuple);
-
- // Resolved here rather than taken from the caller, so that this method can be reached directly from a test:
- // on the JIT the dispatch in CreateMaterializer always picks the expression tree.
- var valueTupleFieldTypes = GetValueTupleFieldTypes(valueTupleType);
-
- var columnBindings = new ReflectionColumnBinding[dataReader.FieldCount];
-
- for (var fieldOrdinal = 0; fieldOrdinal < dataReader.FieldCount; fieldOrdinal++)
- {
- var dataReaderFieldName = dataReaderFieldNames[fieldOrdinal];
- var dataReaderFieldType = dataReaderFieldTypes[fieldOrdinal];
- var targetType = valueTupleFieldTypes[fieldOrdinal];
-
- columnBindings[fieldOrdinal] = new ReflectionColumnBinding(
- GetColumnNameOrPosition(fieldOrdinal, dataReaderFieldName),
- fieldOrdinal,
- MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction(
- fieldOrdinal,
- dataReaderFieldName,
- dataReaderFieldType
- ),
- dataReaderFieldType != targetType,
- targetType
- );
- }
+ // In C# value tuples with more than 7 fields are represented as nested value tuples.
+ // E.g. a ValueTuple with 15 fields is represented as:
+ // ValueTuple>>
+ // In this case we need to create the nested value tuples from the inside out.
- var valueTupleConstructors = GetValueTupleConstructors(valueTupleType)
- .Select(ConstructorInvoker.Create)
- .ToArray();
+ // So we chunk the field values into groups of 7, which gives us the field values of one of those value
+ // tuples per chunk. The chunks are in the same order as valueTupleConstructors: the first chunk and the
+ // first constructor belong to the outermost value tuple, the last ones to the innermost value tuple.
+ var fieldValueChunks = fieldValues.Chunk(ValueTupleFieldCountBeforeNesting).ToArray();
- return rowDataReader => MaterializeValueTuple(
- rowDataReader,
- valueTupleType,
- valueTupleConstructors,
- columnBindings
- );
- }
+ object? valueTuple = null;
- ///
- /// Creates a materializer function that materializes the data in a to an instance of
- /// the value tuple type .
- ///
- /// The type of value tuple to materialize.
- ///
- /// The field types of the value tuple type .
- ///
- /// The for which to create the materializer function.
- ///
- /// The names of the fields in .
- /// The order of the names must match the order of the fields in .
- ///
- ///
- /// The field types of the fields in .
- /// The order of the types must match the order of the fields in .
- ///
- ///
- /// A function that materializes the data in a to an instance of the value tuple type
- /// .
- ///
- ///
- /// Which of the two implementations builds the materializer is decided here, by
- /// : the compiled expression tree when the runtime can
- /// generate code, and the reflection-based materializer when it cannot. The AOT compiler folds that check to a
- /// constant and removes the branch it does not need, so an application published with Native AOT does not carry
- /// the expression-tree implementation at all.
- ///
-#if !NET9_0_OR_GREATER
- // See the identical suppression in EntityMaterializerFactory.CreateMaterializer for why this is here, why it is
- // a transcription of a result the net10.0 build verifies rather than an assertion, and why both target
- // frameworks have to stay in the AOT warning gate.
- [UnconditionalSuppressMessage(
- "AOT",
- "IL3050:Requires dynamic code",
- Justification =
- "The call is inside an if (RuntimeFeature.IsDynamicCodeSupported) branch, which the AOT compiler folds " +
- "to false and removes together with the expression-tree implementation. The net9.0+ analyzer " +
- "recognizes that guard and reports nothing here; net8.0 lacks the [FeatureGuard] annotation on " +
- "IsDynamicCodeSupported that lets it do so."
- )]
-#endif
- private static Delegate CreateMaterializer<
- [DynamicallyAccessedMembers(ValueTupleMemberTypes)] TValueTuple
- >(
- Type[] valueTupleFieldTypes,
- DbDataReader dataReader,
- String[] dataReaderFieldNames,
- Type[] dataReaderFieldTypes
- )
- {
- if (RuntimeFeature.IsDynamicCodeSupported)
+ // Now we create the nested value tuples from the inside out, by walking both arrays from their last entry
+ // to their first one. When we are done valueTuple contains the outermost value tuple.
+ for (var chunkIndex = fieldValueChunks.Length - 1; chunkIndex >= 0; chunkIndex--)
{
- return CreateExpressionMaterializer(
- valueTupleFieldTypes,
- dataReader,
- dataReaderFieldNames,
- dataReaderFieldTypes
- );
+ // If valueTuple is null, it means we are at the innermost value tuple, so we only need to use the
+ // current chunk of field values as arguments.
+ //
+ // Otherwise, if valueTuple is not null, it means we are not at the innermost value tuple, and we need to
+ // add valueTuple (which contains the last created inner value tuple) as the argument for the "Rest"
+ // parameter.
+ var constructorArguments = valueTuple is not null
+ ? [.. fieldValueChunks[chunkIndex], valueTuple]
+ : fieldValueChunks[chunkIndex];
+
+ valueTuple = valueTupleConstructors[chunkIndex].Invoke(constructorArguments.AsSpan());
}
- return CreateReflectionMaterializer(dataReader, dataReaderFieldNames, dataReaderFieldTypes);
+ return valueTuple!;
}
///
@@ -374,12 +353,7 @@ Type[] dataReaderFieldTypes
[RequiresDynamicCode(MaterializerRequiresDynamicCodeMessage)]
private static Delegate CreateExpressionMaterializer<
[DynamicallyAccessedMembers(ValueTupleMemberTypes)] TValueTuple
- >(
- Type[] valueTupleFieldTypes,
- DbDataReader dataReader,
- String[] dataReaderFieldNames,
- Type[] dataReaderFieldTypes
- )
+ >(Type[] valueTupleFieldTypes, DbDataReader dataReader, string[] dataReaderFieldNames, Type[] dataReaderFieldTypes)
{
var valueTupleType = typeof(TValueTuple);
@@ -452,11 +426,11 @@ Type[] dataReaderFieldTypes
? Expression.Default(targetType)
: Expression.Throw(
Expression.New(
- typeof(InvalidCastException).GetConstructor([typeof(String)])!,
+ typeof(InvalidCastException).GetConstructor([typeof(string)])!,
Expression.Constant(
- $"The {columnNameOrPosition} returned by the SQL statement contains a NULL " +
- $"value, but the corresponding field of the value tuple type {valueTupleType} " +
- "is non-nullable."
+ $"The {columnNameOrPosition} returned by the SQL statement contains a NULL "
+ + $"value, but the corresponding field of the value tuple type {valueTupleType} "
+ + "is non-nullable."
)
),
targetType
@@ -464,14 +438,12 @@ Type[] dataReaderFieldTypes
var throwInvalidCastExceptionExpression = Expression.Throw(
Expression.New(
- typeof(InvalidCastException).GetConstructor(
- [typeof(String), typeof(Exception)]
- )!,
+ typeof(InvalidCastException).GetConstructor([typeof(string), typeof(Exception)])!,
Expression.Constant(
- $"The {columnNameOrPosition} returned by the SQL statement contains a " +
- $"value that could not be converted to the type {targetType} " +
- $"of the corresponding field of the value tuple type {valueTupleType}. " +
- "See inner exception for details."
+ $"The {columnNameOrPosition} returned by the SQL statement contains a "
+ + $"value that could not be converted to the type {targetType} "
+ + $"of the corresponding field of the value tuple type {valueTupleType}. "
+ + "See inner exception for details."
),
exceptionParameterExpression
),
@@ -483,30 +455,25 @@ Type[] dataReaderFieldTypes
Expression.Call(
null,
MaterializerFactoryHelper.MakeValueConverterConvertValueToTypeMethod(targetType),
- Expression.Convert(getFieldValueCallExpression, typeof(Object))
+ Expression.Convert(getFieldValueCallExpression, typeof(object))
),
targetType
),
- Expression.Catch(
- exceptionParameterExpression,
- throwInvalidCastExceptionExpression
- )
+ Expression.Catch(exceptionParameterExpression, throwInvalidCastExceptionExpression)
);
- var isNotDbNullBranchExpression = dataReaderFieldType != targetType
- ? convertFieldValueExpression
- : getFieldValueCallExpression;
+ var isNotDbNullBranchExpression =
+ dataReaderFieldType != targetType ? convertFieldValueExpression : getFieldValueCallExpression;
- dataReaderFieldValueExpressions[fieldOrdinal] =
- Expression.Condition(
- Expression.Call(
- dataReaderParameterExpression,
- MaterializerFactoryHelper.DbDataReaderIsDBNullMethod,
- fieldOrdinalExpression
- ),
- isDbNullBranchExpression,
- isNotDbNullBranchExpression
- );
+ dataReaderFieldValueExpressions[fieldOrdinal] = Expression.Condition(
+ Expression.Call(
+ dataReaderParameterExpression,
+ MaterializerFactoryHelper.DbDataReaderIsDBNullMethod,
+ fieldOrdinalExpression
+ ),
+ isDbNullBranchExpression,
+ isNotDbNullBranchExpression
+ );
}
// In C# value tuples with more than 7 fields are represented as nested value tuples.
@@ -516,8 +483,9 @@ Type[] dataReaderFieldTypes
// First we chunk the field value expressions into groups of 7.
// We use a stack to reverse the order, so we start with the expressions for the most inner value tuple.
- var fieldValueExpressionChunks =
- new Stack(dataReaderFieldValueExpressions.Chunk(ValueTupleFieldCountBeforeNesting));
+ var fieldValueExpressionChunks = new Stack(
+ dataReaderFieldValueExpressions.Chunk(ValueTupleFieldCountBeforeNesting)
+ );
// Then we get the constructors for the value tuple types, which GetValueTupleConstructors returns from the
// outermost to the innermost. Pushing them onto a stack in that order reverses it, so we start with the
@@ -542,15 +510,73 @@ Type[] dataReaderFieldTypes
// the "Rest" parameter.
var arguments = newExpression is not null ? [.. chunk, newExpression] : chunk;
- newExpression = Expression.New(
- constructor,
- arguments
- );
+ newExpression = Expression.New(constructor, arguments);
}
return Expression.Lambda(newExpression!, dataReaderParameterExpression).Compile();
}
+ ///
+ /// Creates a materializer function that materializes the data in a to an instance of
+ /// the value tuple type .
+ ///
+ /// The type of value tuple to materialize.
+ ///
+ /// The field types of the value tuple type .
+ ///
+ /// The for which to create the materializer function.
+ ///
+ /// The names of the fields in .
+ /// The order of the names must match the order of the fields in .
+ ///
+ ///
+ /// The field types of the fields in .
+ /// The order of the types must match the order of the fields in .
+ ///
+ ///
+ /// A function that materializes the data in a to an instance of the value tuple type
+ /// .
+ ///
+ ///
+ /// Which of the two implementations builds the materializer is decided here, by
+ /// : the compiled expression tree when the runtime can
+ /// generate code, and the reflection-based materializer when it cannot. The AOT compiler folds that check to a
+ /// constant and removes the branch it does not need, so an application published with Native AOT does not carry
+ /// the expression-tree implementation at all.
+ ///
+#if !NET9_0_OR_GREATER
+ // See the identical suppression in EntityMaterializerFactory.CreateMaterializer for why this is here, why it is
+ // a transcription of a result the net10.0 build verifies rather than an assertion, and why both target
+ // frameworks have to stay in the AOT warning gate.
+ [UnconditionalSuppressMessage(
+ "AOT",
+ "IL3050:Requires dynamic code",
+ Justification = "The call is inside an if (RuntimeFeature.IsDynamicCodeSupported) branch, which the AOT compiler folds "
+ + "to false and removes together with the expression-tree implementation. The net9.0+ analyzer "
+ + "recognizes that guard and reports nothing here; net8.0 lacks the [FeatureGuard] annotation on "
+ + "IsDynamicCodeSupported that lets it do so."
+ )]
+#endif
+ private static Delegate CreateMaterializer<[DynamicallyAccessedMembers(ValueTupleMemberTypes)] TValueTuple>(
+ Type[] valueTupleFieldTypes,
+ DbDataReader dataReader,
+ string[] dataReaderFieldNames,
+ Type[] dataReaderFieldTypes
+ )
+ {
+ if (RuntimeFeature.IsDynamicCodeSupported)
+ {
+ return CreateExpressionMaterializer(
+ valueTupleFieldTypes,
+ dataReader,
+ dataReaderFieldNames,
+ dataReaderFieldTypes
+ );
+ }
+
+ return CreateReflectionMaterializer(dataReader, dataReaderFieldNames, dataReaderFieldTypes);
+ }
+
///
/// Gets the description of the field with the ordinal that the exception
/// messages refer the consumer to.
@@ -561,8 +587,8 @@ Type[] dataReaderFieldTypes
/// The name of the field, or - for a result set whose columns have no name, which a value tuple query is allowed
/// to have because its fields are matched by position - the position of the field.
///
- private static String GetColumnNameOrPosition(Int32 fieldOrdinal, String? dataReaderFieldName) =>
- !String.IsNullOrWhiteSpace(dataReaderFieldName)
+ private static string GetColumnNameOrPosition(int fieldOrdinal, string? dataReaderFieldName) =>
+ !string.IsNullOrWhiteSpace(dataReaderFieldName)
? $"column '{dataReaderFieldName}'"
: $"{(fieldOrdinal + 1).OrdinalizeEnglish()} column";
@@ -612,13 +638,13 @@ private static String GetColumnNameOrPosition(Int32 fieldOrdinal, String? dataRe
[UnconditionalSuppressMessage(
"Trimming",
"IL2065:Value passed to implicit 'this' parameter cannot be statically determined",
- Justification =
- "The nested value tuple types this walks are System.ValueTuple`1-`8, whose constructors the embedded " +
- "ILLink.Descriptors.xml preserves in a consumer's trimmed or Native AOT publish. The caller has already " +
- "rejected any type that is not a value tuple, and a unit test guards the descriptor's completeness."
+ Justification = "The nested value tuple types this walks are System.ValueTuple`1-`8, whose constructors the embedded "
+ + "ILLink.Descriptors.xml preserves in a consumer's trimmed or Native AOT publish. The caller has already "
+ + "rejected any type that is not a value tuple, and a unit test guards the descriptor's completeness."
)]
private static ConstructorInfo[] GetValueTupleConstructors(
- [DynamicallyAccessedMembers(ValueTupleMemberTypes)] Type valueTupleType)
+ [DynamicallyAccessedMembers(ValueTupleMemberTypes)] Type valueTupleType
+ )
{
var valueTupleConstructors = new List();
@@ -630,10 +656,7 @@ private static ConstructorInfo[] GetValueTupleConstructors(
var genericArguments = currentValueTupleType.GetGenericArguments();
valueTupleConstructors.Add(
- currentValueTupleType.GetConstructor(
- BindingFlags.Public | BindingFlags.Instance,
- genericArguments
- )!
+ currentValueTupleType.GetConstructor(BindingFlags.Public | BindingFlags.Instance, genericArguments)!
);
// Fewer than eight arguments means there is no "Rest" field, so this is the innermost value tuple type.
@@ -649,6 +672,50 @@ private static ConstructorInfo[] GetValueTupleConstructors(
return [.. valueTupleConstructors];
}
+ ///
+ ///
+ /// Gets the types of the fields of the value tuple type including the fields
+ /// of all nested value tuple types.
+ ///
+ ///
+ /// For example, for the value tuple type
+ /// ]]> this method returns the types
+ /// T1, T2, T3, T4, T5, T6, T7, T8, T9.
+ ///
+ ///
+ /// The value tuple type of which to get the field types.
+ ///
+ /// An array containing the types of the fields of the value tuple type
+ /// including the fields of all nested value tuple types.
+ ///
+ private static Type[] GetValueTupleFieldTypes(
+ [DynamicallyAccessedMembers(ValueTupleMemberTypes)] Type valueTupleType
+ )
+ {
+ var fieldTypes = new List();
+ var currentValueTupleType = valueTupleType;
+
+ while (true)
+ {
+ // The generic arguments of a value tuple type ARE its field types, in field order.
+ var genericArguments = currentValueTupleType.GetGenericArguments();
+
+ var hasNestedValueTuple = genericArguments.Length > ValueTupleFieldCountBeforeNesting;
+
+ if (!hasNestedValueTuple)
+ {
+ fieldTypes.AddRange(genericArguments);
+ break;
+ }
+
+ fieldTypes.AddRange(genericArguments.Take(ValueTupleFieldCountBeforeNesting));
+
+ currentValueTupleType = genericArguments[^1];
+ }
+
+ return [.. fieldTypes];
+ }
+
///
/// Materializes the current row of to an instance of the value tuple type
/// .
@@ -685,87 +752,6 @@ ReflectionColumnBinding[] columnBindings
return (TValueTuple)ConstructValueTuple(valueTupleConstructors, fieldValues);
}
- ///
- /// Reads all fields of the current row of , in the order of the result set.
- ///
- /// The to read the current row of.
- ///
- /// The type of value tuple being materialized. Used in the exception messages.
- ///
- /// The fields of the result set, in the order of the fields of the value tuple.
- ///
- /// The field values, in the order of the result set and therefore in the order of the fields of the value tuple,
- /// each one ready to be passed to the constructor of the value tuple that holds it.
- ///
- ///
- /// A field of the result set could not be assigned to the corresponding field of the value tuple.
- ///
- private static Object?[] ReadFieldValues(
- DbDataReader dataReader,
- Type valueTupleType,
- ReflectionColumnBinding[] columnBindings
- )
- {
- var fieldValues = new Object?[columnBindings.Length];
-
- for (var fieldOrdinal = 0; fieldOrdinal < columnBindings.Length; fieldOrdinal++)
- {
- fieldValues[fieldOrdinal] = ReadFieldValue(dataReader, valueTupleType, columnBindings[fieldOrdinal]);
- }
-
- return fieldValues;
- }
-
- ///
- /// Constructs the value tuple that holds .
- ///
- ///
- /// The constructors of the value tuple types, from the outermost to the innermost, as returned by
- /// .
- ///
- ///
- /// The value of every field of the value tuple, including the fields of all nested value tuples, in the order in
- /// which the fields are declared.
- ///
- /// The constructed value tuple, boxed.
- ///
- /// This does the same as the tail of , which builds the
- /// same nesting out of instead of constructing it.
- ///
- private static Object ConstructValueTuple(ConstructorInvoker[] valueTupleConstructors, Object?[] fieldValues)
- {
- // In C# value tuples with more than 7 fields are represented as nested value tuples.
- // E.g. a ValueTuple with 15 fields is represented as:
- // ValueTuple>>
- // In this case we need to create the nested value tuples from the inside out.
-
- // So we chunk the field values into groups of 7, which gives us the field values of one of those value
- // tuples per chunk. The chunks are in the same order as valueTupleConstructors: the first chunk and the
- // first constructor belong to the outermost value tuple, the last ones to the innermost value tuple.
- var fieldValueChunks = fieldValues.Chunk(ValueTupleFieldCountBeforeNesting).ToArray();
-
- Object? valueTuple = null;
-
- // Now we create the nested value tuples from the inside out, by walking both arrays from their last entry
- // to their first one. When we are done valueTuple contains the outermost value tuple.
- for (var chunkIndex = fieldValueChunks.Length - 1; chunkIndex >= 0; chunkIndex--)
- {
- // If valueTuple is null, it means we are at the innermost value tuple, so we only need to use the
- // current chunk of field values as arguments.
- //
- // Otherwise, if valueTuple is not null, it means we are not at the innermost value tuple, and we need to
- // add valueTuple (which contains the last created inner value tuple) as the argument for the "Rest"
- // parameter.
- var constructorArguments = valueTuple is not null
- ? [.. fieldValueChunks[chunkIndex], valueTuple]
- : fieldValueChunks[chunkIndex];
-
- valueTuple = valueTupleConstructors[chunkIndex].Invoke(constructorArguments.AsSpan());
- }
-
- return valueTuple!;
- }
-
///
/// Reads the value of the field described by from the current row of
/// , converting it to the type of the value tuple field it is assigned to.
@@ -790,7 +776,7 @@ private static Object ConstructValueTuple(ConstructorInvoker[] valueTupleConstru
///
///
///
- private static Object? ReadFieldValue(
+ private static object? ReadFieldValue(
DbDataReader dataReader,
Type valueTupleType,
ReflectionColumnBinding columnBinding
@@ -804,9 +790,9 @@ ReflectionColumnBinding columnBinding
}
throw new InvalidCastException(
- $"The {columnBinding.ColumnNameOrPosition} returned by the SQL statement contains a NULL " +
- $"value, but the corresponding field of the value tuple type {valueTupleType} " +
- "is non-nullable."
+ $"The {columnBinding.ColumnNameOrPosition} returned by the SQL statement contains a NULL "
+ + $"value, but the corresponding field of the value tuple type {valueTupleType} "
+ + "is non-nullable."
);
}
@@ -824,56 +810,44 @@ ReflectionColumnBinding columnBinding
catch (Exception exception)
{
throw new InvalidCastException(
- $"The {columnBinding.ColumnNameOrPosition} returned by the SQL statement contains a " +
- $"value that could not be converted to the type {columnBinding.TargetType} " +
- $"of the corresponding field of the value tuple type {valueTupleType}. " +
- "See inner exception for details.",
+ $"The {columnBinding.ColumnNameOrPosition} returned by the SQL statement contains a "
+ + $"value that could not be converted to the type {columnBinding.TargetType} "
+ + $"of the corresponding field of the value tuple type {valueTupleType}. "
+ + "See inner exception for details.",
exception
);
}
}
///
- ///
- /// Gets the types of the fields of the value tuple type including the fields
- /// of all nested value tuple types.
- ///
- ///
- /// For example, for the value tuple type
- /// ]]> this method returns the types
- /// T1, T2, T3, T4, T5, T6, T7, T8, T9.
- ///
+ /// Reads all fields of the current row of , in the order of the result set.
///
- /// The value tuple type of which to get the field types.
+ /// The to read the current row of.
+ ///
+ /// The type of value tuple being materialized. Used in the exception messages.
+ ///
+ /// The fields of the result set, in the order of the fields of the value tuple.
///
- /// An array containing the types of the fields of the value tuple type
- /// including the fields of all nested value tuple types.
+ /// The field values, in the order of the result set and therefore in the order of the fields of the value tuple,
+ /// each one ready to be passed to the constructor of the value tuple that holds it.
///
- private static Type[] GetValueTupleFieldTypes(
- [DynamicallyAccessedMembers(ValueTupleMemberTypes)] Type valueTupleType)
+ ///
+ /// A field of the result set could not be assigned to the corresponding field of the value tuple.
+ ///
+ private static object?[] ReadFieldValues(
+ DbDataReader dataReader,
+ Type valueTupleType,
+ ReflectionColumnBinding[] columnBindings
+ )
{
- var fieldTypes = new List();
- var currentValueTupleType = valueTupleType;
+ var fieldValues = new object?[columnBindings.Length];
- while (true)
+ for (var fieldOrdinal = 0; fieldOrdinal < columnBindings.Length; fieldOrdinal++)
{
- // The generic arguments of a value tuple type ARE its field types, in field order.
- var genericArguments = currentValueTupleType.GetGenericArguments();
-
- var hasNestedValueTuple = genericArguments.Length > ValueTupleFieldCountBeforeNesting;
-
- if (!hasNestedValueTuple)
- {
- fieldTypes.AddRange(genericArguments);
- break;
- }
-
- fieldTypes.AddRange(genericArguments.Take(ValueTupleFieldCountBeforeNesting));
-
- currentValueTupleType = genericArguments[^1];
+ fieldValues[fieldOrdinal] = ReadFieldValue(dataReader, valueTupleType, columnBindings[fieldOrdinal]);
}
- return [.. fieldTypes];
+ return fieldValues;
}
///
@@ -924,7 +898,7 @@ private static void ValidateDataReader(
[DynamicallyAccessedMembers(ValueTupleMemberTypes)] Type valueTupleType,
Type[] valueTupleFieldTypes,
DbDataReader dataReader,
- String[] dataReaderFieldNames,
+ string[] dataReaderFieldNames,
Type[] dataReaderFieldTypes
)
{
@@ -936,9 +910,9 @@ Type[] dataReaderFieldTypes
if (dataReader.FieldCount != valueTupleFieldTypes.Length)
{
throw new ArgumentException(
- $"The SQL statement returned {"column".ToQuantity(dataReader.FieldCount)}, but the value tuple type " +
- $"{valueTupleType} has {"field".ToQuantity(valueTupleFieldTypes.Length)}. Make sure that the SQL " +
- "statement returns the same number of columns as the number of fields in the value tuple type.",
+ $"The SQL statement returned {"column".ToQuantity(dataReader.FieldCount)}, but the value tuple type "
+ + $"{valueTupleType} has {"field".ToQuantity(valueTupleFieldTypes.Length)}. Make sure that the SQL "
+ + "statement returns the same number of columns as the number of fields in the value tuple type.",
nameof(dataReader)
);
}
@@ -955,9 +929,9 @@ Type[] dataReaderFieldTypes
if (!ValueConverter.CanConvert(dataReaderFieldType, valueTupleFieldType))
{
throw new ArgumentException(
- $"The data type {dataReaderFieldType} of the {columnNameOrPosition} returned by the SQL " +
- $"statement is not compatible with the field type {valueTupleFieldType} of the corresponding " +
- $"field of the value tuple type {valueTupleType}.",
+ $"The data type {dataReaderFieldType} of the {columnNameOrPosition} returned by the SQL "
+ + $"statement is not compatible with the field type {valueTupleFieldType} of the corresponding "
+ + $"field of the value tuple type {valueTupleType}.",
nameof(dataReader)
);
}
@@ -965,16 +939,14 @@ Type[] dataReaderFieldTypes
if (!MaterializerFactoryHelper.IsDbDataReaderTypedGetMethodAvailable(dataReaderFieldType))
{
throw new ArgumentException(
- $"The data type {dataReaderFieldType} of the {columnNameOrPosition} returned by the SQL " +
- "statement is not supported.",
+ $"The data type {dataReaderFieldType} of the {columnNameOrPosition} returned by the SQL "
+ + "statement is not supported.",
nameof(dataReader)
);
}
}
}
- private static readonly ConcurrentDictionary materializerCache = [];
-
///
/// A cache key used to uniquely identify a value tuple materializer.
///
@@ -989,27 +961,28 @@ Type[] dataReaderFieldTypes
///
private readonly struct MaterializerCacheKey(
Type[] valueTupleFieldTypes,
- String[] dataReaderFieldNames,
+ string[] dataReaderFieldNames,
Type[] dataReaderFieldTypes
- )
- : IEquatable
+ ) : IEquatable
{
+ private string[] DataReaderFieldNames { get; } = dataReaderFieldNames;
+ private Type[] DataReaderFieldTypes { get; } = dataReaderFieldTypes;
+ private Type[] ValueTupleFieldTypes { get; } = valueTupleFieldTypes;
+
///
- public Boolean Equals(MaterializerCacheKey other) =>
- this.ValueTupleFieldTypes.SequenceEqual(other.ValueTupleFieldTypes) &&
- this.DataReaderFieldNames.SequenceEqual(other.DataReaderFieldNames) &&
- this.DataReaderFieldTypes.SequenceEqual(other.DataReaderFieldTypes);
+ public bool Equals(MaterializerCacheKey other) =>
+ this.ValueTupleFieldTypes.SequenceEqual(other.ValueTupleFieldTypes)
+ && this.DataReaderFieldNames.SequenceEqual(other.DataReaderFieldNames)
+ && this.DataReaderFieldTypes.SequenceEqual(other.DataReaderFieldTypes);
///
- public override Boolean Equals(Object? obj) =>
- obj is MaterializerCacheKey other && this.Equals(other);
+ public override bool Equals(object? obj) => obj is MaterializerCacheKey other && this.Equals(other);
///
- public override Int32 GetHashCode()
+ public override int GetHashCode()
{
var hashCode = new HashCode();
-
foreach (var fieldType in this.ValueTupleFieldTypes)
{
hashCode.Add(fieldType);
@@ -1027,10 +1000,6 @@ public override Int32 GetHashCode()
return hashCode.ToHashCode();
}
-
- private String[] DataReaderFieldNames { get; } = dataReaderFieldNames;
- private Type[] DataReaderFieldTypes { get; } = dataReaderFieldTypes;
- private Type[] ValueTupleFieldTypes { get; } = valueTupleFieldTypes;
}
///
@@ -1054,10 +1023,10 @@ public override Int32 GetHashCode()
/// The type the field value is converted to - the type of the value tuple field it is assigned to.
///
private readonly record struct ReflectionColumnBinding(
- String ColumnNameOrPosition,
- Int32 FieldOrdinal,
- Func GetFieldValue,
- Boolean NeedsConversion,
+ string ColumnNameOrPosition,
+ int FieldOrdinal,
+ Func GetFieldValue,
+ bool NeedsConversion,
Type TargetType
);
}
diff --git a/src/DbConnectionPlus/Readers/CommandDisposingDataReaderDecorator.cs b/src/DbConnectionPlus/Readers/CommandDisposingDataReaderDecorator.cs
index f838932..5e74f33 100644
--- a/src/DbConnectionPlus/Readers/CommandDisposingDataReaderDecorator.cs
+++ b/src/DbConnectionPlus/Readers/CommandDisposingDataReaderDecorator.cs
@@ -12,6 +12,12 @@ namespace RentADeveloper.DbConnectionPlus.Readers;
///
internal sealed class CommandDisposingDataReaderDecorator : DbDataReader
{
+ private readonly CancellationToken commandCancellationToken;
+ private readonly DbCommandDisposer commandDisposer;
+ private readonly DbDataReader dataReader;
+ private readonly IDatabaseAdapter databaseAdapter;
+ private bool isDisposed;
+
///
/// Initializes a new instance of the class.
///
@@ -58,36 +64,34 @@ CancellationToken commandCancellationToken
}
///
- public override Int32 Depth => this.dataReader.Depth;
+ public override int Depth => this.dataReader.Depth;
///
- public override Int32 FieldCount => this.dataReader.FieldCount;
+ public override int FieldCount => this.dataReader.FieldCount;
///
- public override Boolean HasRows => this.dataReader.HasRows;
+ public override bool HasRows => this.dataReader.HasRows;
///
- public override Boolean IsClosed => this.dataReader.IsClosed;
+ public override bool IsClosed => this.dataReader.IsClosed;
///
- public override Object this[Int32 ordinal] => this.dataReader[ordinal];
+ public override int RecordsAffected => this.dataReader.RecordsAffected;
///
- public override Object this[String name] => this.dataReader[name];
+ public override int VisibleFieldCount => this.dataReader.VisibleFieldCount;
///
- public override Int32 RecordsAffected => this.dataReader.RecordsAffected;
+ public override object this[int ordinal] => this.dataReader[ordinal];
///
- public override Int32 VisibleFieldCount => this.dataReader.VisibleFieldCount;
+ public override object this[string name] => this.dataReader[name];
///
- public override void Close() =>
- this.dataReader.Close();
+ public override void Close() => this.dataReader.Close();
///
- public override Task CloseAsync() =>
- this.dataReader.CloseAsync();
+ public override Task CloseAsync() => this.dataReader.CloseAsync();
///
public override async ValueTask DisposeAsync()
@@ -105,176 +109,137 @@ public override async ValueTask DisposeAsync()
}
///
- public override Boolean GetBoolean(Int32 ordinal) =>
- this.dataReader.GetBoolean(ordinal);
+ public override bool GetBoolean(int ordinal) => this.dataReader.GetBoolean(ordinal);
///
- public override Byte GetByte(Int32 ordinal) =>
- this.dataReader.GetByte(ordinal);
+ public override byte GetByte(int ordinal) => this.dataReader.GetByte(ordinal);
///
- public override Int64 GetBytes(
- Int32 ordinal,
- Int64 dataOffset,
- Byte[]? buffer,
- Int32 bufferOffset,
- Int32 length
- ) =>
+ public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) =>
this.dataReader.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length);
///
- public override Char GetChar(Int32 ordinal) =>
- this.dataReader.GetChar(ordinal);
+ public override char GetChar(int ordinal) => this.dataReader.GetChar(ordinal);
///
- public override Int64 GetChars(
- Int32 ordinal,
- Int64 dataOffset,
- Char[]? buffer,
- Int32 bufferOffset,
- Int32 length
- ) =>
+ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) =>
this.dataReader.GetChars(ordinal, dataOffset, buffer, bufferOffset, length);
///
public override Task> GetColumnSchemaAsync(
CancellationToken cancellationToken = default
- ) =>
- this.dataReader.GetColumnSchemaAsync(cancellationToken);
+ ) => this.dataReader.GetColumnSchemaAsync(cancellationToken);
///
- public override String GetDataTypeName(Int32 ordinal) =>
- this.dataReader.GetDataTypeName(ordinal);
+ public override string GetDataTypeName(int ordinal) => this.dataReader.GetDataTypeName(ordinal);
///
- public override DateTime GetDateTime(Int32 ordinal) =>
- this.dataReader.GetDateTime(ordinal);
+ public override DateTime GetDateTime(int ordinal) => this.dataReader.GetDateTime(ordinal);
///
- public override Decimal GetDecimal(Int32 ordinal) =>
- this.dataReader.GetDecimal(ordinal);
+ public override decimal GetDecimal(int ordinal) => this.dataReader.GetDecimal(ordinal);
///
- public override Double GetDouble(Int32 ordinal) =>
- this.dataReader.GetDouble(ordinal);
+ public override double GetDouble(int ordinal) => this.dataReader.GetDouble(ordinal);
///
- public override IEnumerator GetEnumerator() =>
- this.dataReader.GetEnumerator();
+ public override IEnumerator GetEnumerator() => this.dataReader.GetEnumerator();
///
[return: DynamicallyAccessedMembers(
- DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
- public override Type GetFieldType(Int32 ordinal) =>
- this.dataReader.GetFieldType(ordinal);
+ DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties
+ )]
+ public override Type GetFieldType(int ordinal) => this.dataReader.GetFieldType(ordinal);
///
- public override T GetFieldValue(Int32 ordinal) =>
- this.dataReader.GetFieldValue(ordinal);
+ public override T GetFieldValue(int ordinal) => this.dataReader.GetFieldValue(ordinal);
///
- public override Task GetFieldValueAsync(Int32 ordinal, CancellationToken cancellationToken) =>
+ public override Task GetFieldValueAsync(int ordinal, CancellationToken cancellationToken) =>
this.dataReader.GetFieldValueAsync(ordinal, cancellationToken);
///
- public override Single GetFloat(Int32 ordinal) =>
- this.dataReader.GetFloat(ordinal);
+ public override float GetFloat(int ordinal) => this.dataReader.GetFloat(ordinal);
///
- public override Guid GetGuid(Int32 ordinal) =>
- this.dataReader.GetGuid(ordinal);
+ public override Guid GetGuid(int ordinal) => this.dataReader.GetGuid(ordinal);
///
- public override Int16 GetInt16(Int32 ordinal) =>
- this.dataReader.GetInt16(ordinal);
+ public override short GetInt16(int ordinal) => this.dataReader.GetInt16(ordinal);
///
- public override Int32 GetInt32(Int32 ordinal) =>
- this.dataReader.GetInt32(ordinal);
+ public override int GetInt32(int ordinal) => this.dataReader.GetInt32(ordinal);
///
- public override Int64 GetInt64(Int32 ordinal) =>
- this.dataReader.GetInt64(ordinal);
+ public override long GetInt64(int ordinal) => this.dataReader.GetInt64(ordinal);
///
- public override String GetName(Int32 ordinal) =>
- this.dataReader.GetName(ordinal);
+ public override string GetName(int ordinal) => this.dataReader.GetName(ordinal);
///
- public override Int32 GetOrdinal(String name) =>
- this.dataReader.GetOrdinal(name);
+ public override int GetOrdinal(string name) => this.dataReader.GetOrdinal(name);
///
[return: DynamicallyAccessedMembers(
- DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
- public override Type GetProviderSpecificFieldType(Int32 ordinal) =>
+ DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties
+ )]
+ public override Type GetProviderSpecificFieldType(int ordinal) =>
this.dataReader.GetProviderSpecificFieldType(ordinal);
///
- public override Object GetProviderSpecificValue(Int32 ordinal) =>
- this.dataReader.GetProviderSpecificValue(ordinal);
+ public override object GetProviderSpecificValue(int ordinal) => this.dataReader.GetProviderSpecificValue(ordinal);
///
- public override Int32 GetProviderSpecificValues(Object[] values) =>
- this.dataReader.GetProviderSpecificValues(values);
+ public override int GetProviderSpecificValues(object[] values) => this.dataReader.GetProviderSpecificValues(values);
///
- public override DataTable? GetSchemaTable() =>
- this.dataReader.GetSchemaTable();
+ public override DataTable? GetSchemaTable() => this.dataReader.GetSchemaTable();
///
public override Task GetSchemaTableAsync(CancellationToken cancellationToken = default) =>
this.dataReader.GetSchemaTableAsync(cancellationToken);
///
- public override Stream GetStream(Int32 ordinal) =>
- this.dataReader.GetStream(ordinal);
+ public override Stream GetStream(int ordinal) => this.dataReader.GetStream(ordinal);
///
- public override String GetString(Int32 ordinal) =>
- this.dataReader.GetString(ordinal);
+ public override string GetString(int ordinal) => this.dataReader.GetString(ordinal);
///
- public override TextReader GetTextReader(Int32 ordinal) =>
- this.dataReader.GetTextReader(ordinal);
+ public override TextReader GetTextReader(int ordinal) => this.dataReader.GetTextReader(ordinal);
///
- public override Object GetValue(Int32 ordinal) =>
- this.dataReader.GetValue(ordinal);
+ public override object GetValue(int ordinal) => this.dataReader.GetValue(ordinal);
///
- public override Int32 GetValues(Object[] values) =>
- this.dataReader.GetValues(values);
+ public override int GetValues(object[] values) => this.dataReader.GetValues(values);
///
- public override Boolean IsDBNull(Int32 ordinal) =>
- this.dataReader.IsDBNull(ordinal);
+ public override bool IsDBNull(int ordinal) => this.dataReader.IsDBNull(ordinal);
///
- public override Task IsDBNullAsync(Int32 ordinal, CancellationToken cancellationToken) =>
+ public override Task IsDBNullAsync(int ordinal, CancellationToken cancellationToken) =>
this.dataReader.IsDBNullAsync(ordinal, cancellationToken);
///
- public override Boolean NextResult() =>
- this.dataReader.NextResult();
+ public override bool NextResult() => this.dataReader.NextResult();
///
- public override Task NextResultAsync(CancellationToken cancellationToken) =>
+ public override Task NextResultAsync(CancellationToken cancellationToken) =>
this.dataReader.NextResultAsync(cancellationToken);
///
///
/// The operation was canceled via a .
///
- public override Boolean Read()
+ public override bool Read()
{
try
{
return this.dataReader.Read();
}
catch (Exception exception)
- when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
exception,
this.commandCancellationToken
)
@@ -288,26 +253,19 @@ public override Boolean Read()
///
/// The operation was canceled via a .
///
- public override async Task ReadAsync(CancellationToken cancellationToken)
+ public override async Task ReadAsync(CancellationToken cancellationToken)
{
try
{
return await this.dataReader.ReadAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception exception)
- when (
- this.databaseAdapter
- .WasSqlStatementCancelledByCancellationToken(
- exception,
- cancellationToken
- )
- )
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken))
{
throw new OperationCanceledException(cancellationToken);
}
catch (Exception exception)
- when (
- this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
+ when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(
exception,
this.commandCancellationToken
)
@@ -318,11 +276,10 @@ public override async Task ReadAsync(CancellationToken cancellationToke
}
///
- public override String? ToString() =>
- this.dataReader.ToString();
+ public override string? ToString() => this.dataReader.ToString();
///
- protected override void Dispose(Boolean disposing)
+ protected override void Dispose(bool disposing)
{
if (this.isDisposed)
{
@@ -339,10 +296,4 @@ protected override void Dispose(Boolean disposing)
this.commandDisposer.Dispose();
}
}
-
- private readonly CancellationToken commandCancellationToken;
- private readonly DbCommandDisposer commandDisposer;
- private readonly IDatabaseAdapter databaseAdapter;
- private readonly DbDataReader dataReader;
- private Boolean isDisposed;
}
diff --git a/src/DbConnectionPlus/Readers/EnumerableReader.cs b/src/DbConnectionPlus/Readers/EnumerableReader.cs
index 1b5dcd6..6b1a427 100644
--- a/src/DbConnectionPlus/Readers/EnumerableReader.cs
+++ b/src/DbConnectionPlus/Readers/EnumerableReader.cs
@@ -21,6 +21,21 @@ namespace RentADeveloper.DbConnectionPlus.Readers;
///
internal sealed class EnumerableReader : DbDataReader
{
+ private readonly IEnumerator enumerator;
+ private readonly string[] fieldNames;
+ private readonly EnumerableReaderOptions options;
+ private readonly EntityPropertyMetadata[] properties;
+
+ [DynamicallyAccessedMembers(
+ DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties
+ )]
+ private readonly Type? valuesType;
+
+ private object? current;
+ private bool isClosed;
+ private bool isDisposed;
+ private bool isEnumeratorDisposed;
+
///
/// Initializes a new instance of the class that reads a single column, where
/// each element of is the value of that column.
@@ -55,9 +70,11 @@ internal sealed class EnumerableReader : DbDataReader
public EnumerableReader(
IEnumerable values,
[DynamicallyAccessedMembers(
- DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
- Type valuesType,
- String fieldName)
+ DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties
+ )]
+ Type valuesType,
+ string fieldName
+ )
{
ArgumentNullException.ThrowIfNull(values);
ArgumentNullException.ThrowIfNull(valuesType);
@@ -79,10 +96,10 @@ public EnumerableReader(
///
/// The sequence of entities from which the reader will read values.
///
- /// The metadata of the properties that become the columns of the reader, in column order. Every entry must be
+ /// The metadata of the properties that become the columns of the reader in column order. Every entry must be
/// readable, that is, expose a .
///
- /// The behaviours the reader applies to the values it reads.
+ /// The behaviors the reader applies to the values it reads.
///
///
///
@@ -104,7 +121,8 @@ public EnumerableReader(
public EnumerableReader(
IEnumerable values,
IReadOnlyList properties,
- EnumerableReaderOptions options)
+ EnumerableReaderOptions options
+ )
{
ArgumentNullException.ThrowIfNull(values);
ArgumentNullException.ThrowIfNull(properties);
@@ -135,26 +153,41 @@ public EnumerableReader(
}
///
- public override Int32 Depth => 0;
+ public override int Depth => 0;
///
- public override Int32 FieldCount => this.fieldNames.Length;
+ public override int FieldCount => this.fieldNames.Length;
///
- public override Boolean HasRows => true;
+ public override bool HasRows => true;
///
// ReSharper disable once ConvertToAutoPropertyWithPrivateSetter
- public override Boolean IsClosed => this.isClosed;
+ public override bool IsClosed => this.isClosed;
///
- public override Object this[Int32 ordinal] => this.GetValue(ordinal);
+ public override int RecordsAffected => -1;
+
+ ///
+ /// Gets a value indicating whether the reader reads a single column whose value is the sequence element itself.
+ ///
+ private bool IsSingleColumn => this.valuesType is not null;
+
+ ///
+ /// Gets a value indicating whether the reader returns values as .
+ ///
+ private bool ReadsCharsAsStrings => this.options.HasFlag(EnumerableReaderOptions.ReadCharsAsStrings);
+
+ ///
+ /// Gets a value indicating whether the reader serializes values while reading them.
+ ///
+ private bool SerializesEnums => this.options.HasFlag(EnumerableReaderOptions.SerializeEnums);
///
- public override Object this[String name] => this.GetValue(this.GetOrdinalOrThrow(name));
+ public override object this[int ordinal] => this.GetValue(ordinal);
///
- public override Int32 RecordsAffected => -1;
+ public override object this[string name] => this.GetValue(this.GetOrdinalOrThrow(name));
///
public override void Close()
@@ -169,61 +202,41 @@ public override void Close()
}
///
- public override Boolean GetBoolean(Int32 ordinal) =>
- (Boolean)this.GetValue(ordinal);
+ public override bool GetBoolean(int ordinal) => (bool)this.GetValue(ordinal);
///
- public override Byte GetByte(Int32 ordinal) =>
- (Byte)this.GetValue(ordinal);
+ public override byte GetByte(int ordinal) => (byte)this.GetValue(ordinal);
///
/// Always thrown.
- public override Int64 GetBytes(
- Int32 ordinal,
- Int64 dataOffset,
- Byte[]? buffer,
- Int32 bufferOffset,
- Int32 length
- ) =>
+ public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) =>
throw new NotImplementedException();
///
- public override Char GetChar(Int32 ordinal) =>
- (Char)this.GetValue(ordinal);
+ public override char GetChar(int ordinal) => (char)this.GetValue(ordinal);
///
/// Always thrown.
- public override Int64 GetChars(
- Int32 ordinal,
- Int64 dataOffset,
- Char[]? buffer,
- Int32 bufferOffset,
- Int32 length
- ) =>
+ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) =>
throw new NotImplementedException();
///
///
/// The specified ordinal is not one of the ordinals the reader supports.
///
- public override String GetDataTypeName(Int32 ordinal) =>
- this.GetFieldType(ordinal).Name;
+ public override string GetDataTypeName(int ordinal) => this.GetFieldType(ordinal).Name;
///
- public override DateTime GetDateTime(Int32 ordinal) =>
- (DateTime)this.GetValue(ordinal);
+ public override DateTime GetDateTime(int ordinal) => (DateTime)this.GetValue(ordinal);
///
- public override Decimal GetDecimal(Int32 ordinal) =>
- (Decimal)this.GetValue(ordinal);
+ public override decimal GetDecimal(int ordinal) => (decimal)this.GetValue(ordinal);
///
- public override Double GetDouble(Int32 ordinal) =>
- (Double)this.GetValue(ordinal);
+ public override double GetDouble(int ordinal) => (double)this.GetValue(ordinal);
///
- public override IEnumerator GetEnumerator() =>
- this.enumerator;
+ public override IEnumerator GetEnumerator() => this.enumerator;
///
///
@@ -239,8 +252,9 @@ public override IEnumerator GetEnumerator() =>
/// contract by construction.
///
[return: DynamicallyAccessedMembers(
- DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
- public override Type GetFieldType(Int32 ordinal)
+ DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties
+ )]
+ public override Type GetFieldType(int ordinal)
{
this.EnsureValidFieldOrdinal(ordinal);
@@ -248,39 +262,35 @@ public override Type GetFieldType(Int32 ordinal)
}
///
- public override Single GetFloat(Int32 ordinal) =>
- (Single)this.GetValue(ordinal);
+ public override float GetFloat(int ordinal) => (float)this.GetValue(ordinal);
///
- public override Guid GetGuid(Int32 ordinal) =>
- (Guid)this.GetValue(ordinal);
+ public override Guid GetGuid(int ordinal) => (Guid)this.GetValue(ordinal);
///
- public override Int16 GetInt16(Int32 ordinal) =>
- (Int16)this.GetValue(ordinal);
+ public override short GetInt16(int ordinal) => (short)this.GetValue(ordinal);
///
- public override Int32 GetInt32(Int32 ordinal)
+ public override int GetInt32(int ordinal)
{
var value = this.GetValue(ordinal);
if (this.SerializesEnums && this.IsEnumColumn(ordinal) && value is Enum enumValue)
{
- return (Int32)(Object)enumValue;
+ return (int)(object)enumValue;
}
- return (Int32)value;
+ return (int)value;
}
///
- public override Int64 GetInt64(Int32 ordinal) =>
- (Int64)this.GetValue(ordinal);
+ public override long GetInt64(int ordinal) => (long)this.GetValue(ordinal);
///
///
/// The specified ordinal is not one of the ordinals the reader supports.
///
- public override String GetName(Int32 ordinal)
+ public override string GetName(int ordinal)
{
this.EnsureValidFieldOrdinal(ordinal);
@@ -289,17 +299,15 @@ public override String GetName(Int32 ordinal)
///
///
- /// The reader reads a single column and the specified field name is not the field name
- /// that was passed to the constructor of this class.
+ /// The reader reads a single column, and the specified field name is not the field name
+ /// passed to the constructor of this class.
///
///
/// In multi-column mode an unknown name yields -1 rather than an exception, which is what the
/// bulk-copy APIs of the database providers expect - they probe for columns they may not find.
///
- public override Int32 GetOrdinal(String name) =>
- this.IsSingleColumn
- ? this.GetOrdinalOrThrow(name)
- : Array.IndexOf(this.fieldNames, name);
+ public override int GetOrdinal(string name) =>
+ this.IsSingleColumn ? this.GetOrdinalOrThrow(name) : Array.IndexOf(this.fieldNames, name);
///
/// Always thrown.
@@ -310,11 +318,10 @@ public override Int32 GetOrdinal(String name) =>
/// this project does not suppress. Nothing asks for it either: the bulk-copy APIs of all five providers drive
/// this reader through , and .
///
- public override DataTable GetSchemaTable() =>
- throw new NotImplementedException();
+ public override DataTable GetSchemaTable() => throw new NotImplementedException();
///
- public override String GetString(Int32 ordinal)
+ public override string GetString(int ordinal)
{
var value = this.GetValue(ordinal);
@@ -329,17 +336,17 @@ public override String GetString(Int32 ordinal)
// that GetString is called to retrieve the value. Casting a Char to a String would throw an
// InvalidCastException, so the conversion happens here.
- return (value as Char?)?.ToString() ?? String.Empty;
+ return (value as char?)?.ToString() ?? string.Empty;
}
- return (String)value;
+ return (string)value;
}
///
///
/// The specified ordinal is not one of the ordinals the reader supports.
///
- public override Object GetValue(Int32 ordinal)
+ public override object GetValue(int ordinal)
{
this.EnsureValidFieldOrdinal(ordinal);
@@ -356,10 +363,10 @@ public override Object GetValue(Int32 ordinal)
/// The reader reads a single column and does not have a length of at least 1.
///
///
- /// In multi-column mode a buffer shorter than is filled as far as it reaches and the
+ /// In multi-column mode a buffer shorter than is filled as far as it reaches, and the
/// number of values written is returned, as specifies.
///
- public override Int32 GetValues(Object[] values)
+ public override int GetValues(object[] values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -392,14 +399,13 @@ public override Int32 GetValues(Object[] values)
///
/// The specified ordinal is not one of the ordinals the reader supports.
///
- public override Boolean IsDBNull(Int32 ordinal) =>
- this.GetValue(ordinal) is DBNull;
+ public override bool IsDBNull(int ordinal) => this.GetValue(ordinal) is DBNull;
///
- public override Boolean NextResult() => false;
+ public override bool NextResult() => false;
///
- public override Boolean Read()
+ public override bool Read()
{
if (this.isClosed)
{
@@ -417,7 +423,7 @@ public override Boolean Read()
}
///
- protected override void Dispose(Boolean disposing)
+ protected override void Dispose(bool disposing)
{
if (this.isDisposed)
{
@@ -435,140 +441,146 @@ protected override void Dispose(Boolean disposing)
}
///
- /// Gets a value indicating whether the reader reads a single column whose value is the sequence element itself.
+ /// Maps a non-nullable property type onto the statically known the column is reported as.
///
- private Boolean IsSingleColumn => this.valuesType is not null;
+ /// The non-nullable type of the property the column is mapped to.
+ ///
+ /// The statically known type the column is reported as, or for a type this library does
+ /// not store in a temporary table.
+ ///
+ ///
+ /// This cannot be a dictionary lookup. The result flows into the annotated return value of
+ /// , and a read out of a collection carries no
+ /// annotation, so the trimmer reports IL2073 for it. Only a typeof literal satisfies the contract.
+ ///
+ [return: DynamicallyAccessedMembers(
+ DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties
+ )]
+ private static Type MapBuiltInFieldType(Type propertyType)
+ {
+ if (propertyType == typeof(bool))
+ {
+ return typeof(bool);
+ }
- ///
- /// Gets a value indicating whether the reader returns values as .
- ///
- private Boolean ReadsCharsAsStrings =>
- this.options.HasFlag(EnumerableReaderOptions.ReadCharsAsStrings);
+ if (propertyType == typeof(byte))
+ {
+ return typeof(byte);
+ }
- ///
- /// Gets a value indicating whether the reader serializes values while reading them.
- ///
- private Boolean SerializesEnums =>
- this.options.HasFlag(EnumerableReaderOptions.SerializeEnums);
+ if (propertyType == typeof(byte[]))
+ {
+ return typeof(byte[]);
+ }
- ///
- /// Disposes the enumerator obtained from the enumerable.
- ///
- private void DisposeEnumerator()
- {
- if (this.isEnumeratorDisposed)
+ if (propertyType == typeof(sbyte))
{
- return;
+ return typeof(sbyte);
}
- this.isEnumeratorDisposed = true;
- (this.enumerator as IDisposable)?.Dispose();
- }
+ if (propertyType == typeof(char))
+ {
+ return typeof(char);
+ }
- ///
- /// Throws if the specified ordinal is not one of the ordinals the reader supports.
- ///
- /// The ordinal to check.
- ///
- /// The specified ordinal is not one of the ordinals the reader supports.
- ///
- private void EnsureValidFieldOrdinal(Int32 ordinal)
- {
- if (ordinal >= 0 && ordinal < this.FieldCount)
+ if (propertyType == typeof(decimal))
{
- return;
+ return typeof(decimal);
}
- throw new ArgumentOutOfRangeException(
- nameof(ordinal),
- ordinal,
- this.IsSingleColumn
- ? $"The specified ordinal {ordinal} is not supported. The only supported ordinal is zero."
- : $"The specified ordinal {ordinal} is not supported. The supported ordinals are 0 to " +
- $"{this.FieldCount - 1}."
- );
- }
+ if (propertyType == typeof(double))
+ {
+ return typeof(double);
+ }
- ///
- /// Gets the type of the values the column with the specified ordinal reads.
- ///
- /// The ordinal of the column to inspect.
- ///
- /// The type passed to the constructor if the reader reads a single column; otherwise the type of the property
- /// the column is mapped to.
- ///
- private Type GetColumnType(Int32 ordinal) =>
- this.valuesType ?? this.properties[ordinal].PropertyType;
+ if (propertyType == typeof(float))
+ {
+ return typeof(float);
+ }
- ///
- /// Resolves the ordinal of the specified field name, throwing when the reader does not have such a field.
- ///
- /// The field name to resolve.
- /// The ordinal of the field with the specified name.
- ///
- /// The reader does not have a field with the specified name .
- ///
- private Int32 GetOrdinalOrThrow(String name)
- {
- var ordinal = Array.IndexOf(this.fieldNames, name);
+ if (propertyType == typeof(short))
+ {
+ return typeof(short);
+ }
- if (ordinal >= 0)
+ if (propertyType == typeof(ushort))
{
- return ordinal;
+ return typeof(ushort);
}
- throw new ArgumentOutOfRangeException(
- nameof(name),
- this.IsSingleColumn
- ? $"The specified field name '{name}' is not supported. The only supported field name is " +
- $"'{this.fieldNames[0]}'."
- : $"The specified field name '{name}' is not supported. The supported field names are " +
- $"'{String.Join("', '", this.fieldNames)}'."
- );
- }
+ if (propertyType == typeof(int))
+ {
+ return typeof(int);
+ }
- ///
- /// Determines whether the column with the specified ordinal is mapped to an property.
- ///
- /// The ordinal of the column to inspect.
- ///
- /// if the column is mapped to an property; otherwise,
- /// .
- ///
- private Boolean IsEnumColumn(Int32 ordinal) =>
- this.GetColumnType(ordinal).IsEnumOrNullableEnumType();
+ if (propertyType == typeof(uint))
+ {
+ return typeof(uint);
+ }
- ///
- /// Applies the reader's to a value that was read from an entity.
- ///
- /// The value to serialize.
- /// The serialized value.
- private Object SerializeValue(Object value)
- {
- if (this.SerializesEnums && value is Enum enumValue)
+ if (propertyType == typeof(long))
{
- return EnumSerializer.SerializeEnum(
- enumValue,
- DbConnectionPlusConfiguration.Instance.EnumSerializationMode
- );
+ return typeof(long);
}
- if (this.ReadsCharsAsStrings && value is Char charValue)
+ if (propertyType == typeof(ulong))
{
- // The data readers of all major database systems return the type String for CHAR columns.
- // So we mimic the same behavior for consistency.
+ return typeof(ulong);
+ }
- return charValue.ToString();
+ if (propertyType == typeof(nint))
+ {
+ return typeof(nint);
}
- return value;
+ if (propertyType == typeof(nuint))
+ {
+ return typeof(nuint);
+ }
+
+ if (propertyType == typeof(string))
+ {
+ return typeof(string);
+ }
+
+ if (propertyType == typeof(DateTime))
+ {
+ return typeof(DateTime);
+ }
+
+ if (propertyType == typeof(DateOnly))
+ {
+ return typeof(DateOnly);
+ }
+
+ if (propertyType == typeof(DateTimeOffset))
+ {
+ return typeof(DateTimeOffset);
+ }
+
+ if (propertyType == typeof(TimeSpan))
+ {
+ return typeof(TimeSpan);
+ }
+
+ if (propertyType == typeof(TimeOnly))
+ {
+ return typeof(TimeOnly);
+ }
+
+ if (propertyType == typeof(Guid))
+ {
+ return typeof(Guid);
+ }
+
+ return typeof(object);
}
///
/// Resolves the type a column is reported as from the type of the property it is mapped to.
///
/// The type of the property the column is mapped to.
- /// The behaviours the reader applies to the values it reads.
+ /// The behaviors the reader applies to the values it reads.
/// The type the column is reported as.
///
/// The configured is not a defined value.
@@ -578,33 +590,32 @@ private Object SerializeValue(Object value)
/// the base class puts on
/// ; see the remarks there. A type that is neither a supported built-in
/// type nor covered by — an outside MySQL, most notably — is
- /// reported as . Returning the runtime property type would violate the inherited trimming
+ /// reported as . Returning the runtime property type would violate the inherited trimming
/// contract because PropertyInfo.PropertyType carries no member annotation. No caller inside this library
/// reads that fallback: PostgreSqlTemporaryTableBuilder, the one place that would inspect the reader's
/// field types, derives its NpgsqlDbType values from the entity metadata instead.
///
[return: DynamicallyAccessedMembers(
- DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
+ DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties
+ )]
private static Type MapReportedFieldType(Type propertyType, EnumerableReaderOptions options)
{
if (propertyType.IsEnumOrNullableEnumType())
{
if (!options.HasFlag(EnumerableReaderOptions.SerializeEnums))
{
- return typeof(Object);
+ return typeof(object);
}
var enumSerializationMode = DbConnectionPlusConfiguration.Instance.EnumSerializationMode;
return enumSerializationMode switch
{
- EnumSerializationMode.Strings =>
- typeof(String),
+ EnumSerializationMode.Strings => typeof(string),
- EnumSerializationMode.Integers =>
- typeof(Int32),
+ EnumSerializationMode.Integers => typeof(int),
- _ => ThrowInvalidEnumSerializationModeException(enumSerializationMode)
+ _ => ThrowInvalidEnumSerializationModeException(enumSerializationMode),
};
}
@@ -613,7 +624,7 @@ private static Type MapReportedFieldType(Type propertyType, EnumerableReaderOpti
// The data readers of all major database systems return the type String for CHAR columns.
// So we mimic the same behavior for consistency.
- return typeof(String);
+ return typeof(string);
}
return MapBuiltInFieldType(Nullable.GetUnderlyingType(propertyType) ?? propertyType);
@@ -628,7 +639,8 @@ private static Type MapReportedFieldType(Type propertyType, EnumerableReaderOpti
/// Always thrown.
[DoesNotReturn]
[return: DynamicallyAccessedMembers(
- DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
+ DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties
+ )]
private static Type ThrowInvalidEnumSerializationModeException(EnumSerializationMode enumSerializationMode) =>
throw new ArgumentOutOfRangeException(
nameof(enumSerializationMode),
@@ -637,149 +649,113 @@ private static Type ThrowInvalidEnumSerializationModeException(EnumSerialization
);
///
- /// Maps a non-nullable property type onto the statically known the column is reported as.
+ /// Disposes the enumerator obtained from the enumerable.
///
- /// The non-nullable type of the property the column is mapped to.
- ///
- /// The statically known type the column is reported as, or for a type this library does
- /// not store in a temporary table.
- ///
- ///
- /// This cannot be a dictionary lookup. The result flows into the annotated return value of
- /// , and a read out of a collection carries no
- /// annotation, so the trimmer reports IL2073 for it. Only a typeof literal satisfies the contract.
- ///
- [return: DynamicallyAccessedMembers(
- DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
- private static Type MapBuiltInFieldType(Type propertyType)
+ private void DisposeEnumerator()
{
- if (propertyType == typeof(Boolean))
- {
- return typeof(Boolean);
- }
-
- if (propertyType == typeof(Byte))
- {
- return typeof(Byte);
- }
-
- if (propertyType == typeof(Byte[]))
- {
- return typeof(Byte[]);
- }
-
- if (propertyType == typeof(SByte))
- {
- return typeof(SByte);
- }
-
- if (propertyType == typeof(Char))
- {
- return typeof(Char);
- }
-
- if (propertyType == typeof(Decimal))
- {
- return typeof(Decimal);
- }
-
- if (propertyType == typeof(Double))
- {
- return typeof(Double);
- }
-
- if (propertyType == typeof(Single))
- {
- return typeof(Single);
- }
-
- if (propertyType == typeof(Int16))
- {
- return typeof(Int16);
- }
-
- if (propertyType == typeof(UInt16))
- {
- return typeof(UInt16);
- }
-
- if (propertyType == typeof(Int32))
- {
- return typeof(Int32);
- }
-
- if (propertyType == typeof(UInt32))
+ if (this.isEnumeratorDisposed)
{
- return typeof(UInt32);
+ return;
}
- if (propertyType == typeof(Int64))
- {
- return typeof(Int64);
- }
+ this.isEnumeratorDisposed = true;
+ (this.enumerator as IDisposable)?.Dispose();
+ }
- if (propertyType == typeof(UInt64))
+ ///
+ /// Throws if the specified ordinal is not one of the ordinals the reader supports.
+ ///
+ /// The ordinal to check.
+ ///
+ /// The specified ordinal is not one of the ordinals the reader supports.
+ ///
+ private void EnsureValidFieldOrdinal(int ordinal)
+ {
+ if (ordinal >= 0 && ordinal < this.FieldCount)
{
- return typeof(UInt64);
+ return;
}
- if (propertyType == typeof(IntPtr))
- {
- return typeof(IntPtr);
- }
+ throw new ArgumentOutOfRangeException(
+ nameof(ordinal),
+ ordinal,
+ this.IsSingleColumn
+ ? $"The specified ordinal {ordinal} is not supported. The only supported ordinal is zero."
+ : $"The specified ordinal {ordinal} is not supported. The supported ordinals are 0 to "
+ + $"{this.FieldCount - 1}."
+ );
+ }
- if (propertyType == typeof(UIntPtr))
- {
- return typeof(UIntPtr);
- }
+ ///
+ /// Gets the type of the values the column with the specified ordinal reads.
+ ///
+ /// The ordinal of the column to inspect.
+ ///
+ /// The type passed to the constructor if the reader reads a single column; otherwise the type of the property
+ /// the column is mapped to.
+ ///
+ private Type GetColumnType(int ordinal) => this.valuesType ?? this.properties[ordinal].PropertyType;
- if (propertyType == typeof(String))
- {
- return typeof(String);
- }
+ ///
+ /// Resolves the ordinal of the specified field name, throwing when the reader does not have such a field.
+ ///
+ /// The field name to resolve.
+ /// The ordinal of the field with the specified name.
+ ///
+ /// The reader does not have a field with the specified name .
+ ///
+ private int GetOrdinalOrThrow(string name)
+ {
+ var ordinal = Array.IndexOf(this.fieldNames, name);
- if (propertyType == typeof(DateTime))
+ if (ordinal >= 0)
{
- return typeof(DateTime);
+ return ordinal;
}
- if (propertyType == typeof(DateOnly))
- {
- return typeof(DateOnly);
- }
+ throw new ArgumentOutOfRangeException(
+ nameof(name),
+ this.IsSingleColumn
+ ? $"The specified field name '{name}' is not supported. The only supported field name is "
+ + $"'{this.fieldNames[0]}'."
+ : $"The specified field name '{name}' is not supported. The supported field names are "
+ + $"'{string.Join("', '", this.fieldNames)}'."
+ );
+ }
- if (propertyType == typeof(DateTimeOffset))
- {
- return typeof(DateTimeOffset);
- }
+ ///
+ /// Determines whether the column with the specified ordinal is mapped to an property.
+ ///
+ /// The ordinal of the column to inspect.
+ ///
+ /// if the column is mapped to an property; otherwise,
+ /// .
+ ///
+ private bool IsEnumColumn(int ordinal) => this.GetColumnType(ordinal).IsEnumOrNullableEnumType();
- if (propertyType == typeof(TimeSpan))
+ ///
+ /// Applies the reader's to a value that was read from an entity.
+ ///
+ /// The value to serialize.
+ /// The serialized value.
+ private object SerializeValue(object value)
+ {
+ if (this.SerializesEnums && value is Enum enumValue)
{
- return typeof(TimeSpan);
+ return EnumSerializer.SerializeEnum(
+ enumValue,
+ DbConnectionPlusConfiguration.Instance.EnumSerializationMode
+ );
}
- if (propertyType == typeof(TimeOnly))
+ if (this.ReadsCharsAsStrings && value is char charValue)
{
- return typeof(TimeOnly);
- }
+ // The data readers of all major database systems return the type String for CHAR columns.
+ // So we mimic the same behavior for consistency.
- if (propertyType == typeof(Guid))
- {
- return typeof(Guid);
+ return charValue.ToString();
}
- return typeof(Object);
+ return value;
}
-
- private readonly IEnumerator enumerator;
- private readonly String[] fieldNames;
- private readonly EnumerableReaderOptions options;
- private readonly EntityPropertyMetadata[] properties;
- [DynamicallyAccessedMembers(
- DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
- private readonly Type? valuesType;
- private Object? current;
- private Boolean isClosed;
- private Boolean isDisposed;
- private Boolean isEnumeratorDisposed;
}
diff --git a/src/DbConnectionPlus/Readers/EnumerableReaderOptions.cs b/src/DbConnectionPlus/Readers/EnumerableReaderOptions.cs
index 00aff8c..db755d3 100644
--- a/src/DbConnectionPlus/Readers/EnumerableReaderOptions.cs
+++ b/src/DbConnectionPlus/Readers/EnumerableReaderOptions.cs
@@ -8,7 +8,7 @@ namespace RentADeveloper.DbConnectionPlus.Readers;
///
///
/// These flags exist because the temporary-table builders disagree deliberately: MySQL serializes
-/// and values while it reads them, and SQL Server, SQLite, PostgreSQL and
+/// and values while it reads them, and SQL Server, SQLite, PostgreSQL and
/// Oracle hand the raw values to their bulk-copy APIs. That asymmetry is intentional and predates the AOT work —
/// do not level it out here.
///
@@ -27,8 +27,8 @@ internal enum EnumerableReaderOptions
SerializeEnums = 1,
///
- /// Report columns as and return their values as
- /// , mirroring what the data readers of the major database systems do for CHAR columns.
+ /// Report columns as and return their values as
+ /// , mirroring what the data readers of the major database systems do for CHAR columns.
///
- ReadCharsAsStrings = 2
+ ReadCharsAsStrings = 2,
}
diff --git a/src/DbConnectionPlus/SqlStatements/InterpolatedParameter.cs b/src/DbConnectionPlus/SqlStatements/InterpolatedParameter.cs
index 0558a20..b1f10c3 100644
--- a/src/DbConnectionPlus/SqlStatements/InterpolatedParameter.cs
+++ b/src/DbConnectionPlus/SqlStatements/InterpolatedParameter.cs
@@ -11,5 +11,4 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements;
/// This is if no name could be inferred.
///
/// The value of the parameter.
-public record InterpolatedParameter(String? InferredName, Object? Value)
- : IInterpolatedSqlStatementFragment;
+public record InterpolatedParameter(string? InferredName, object? Value) : IInterpolatedSqlStatementFragment;
diff --git a/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs b/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs
index 9d15306..e925c27 100644
--- a/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs
+++ b/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatement.cs
@@ -22,6 +22,9 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements;
// ReSharper disable once StructCanBeMadeReadOnly
public struct InterpolatedSqlStatement : IEquatable
{
+ private readonly List fragments;
+ private readonly List temporaryTables;
+
///
/// Initializes a new instance of the class.
///
@@ -34,7 +37,7 @@ public struct InterpolatedSqlStatement : IEquatable
[EditorBrowsable(EditorBrowsableState.Never)]
// ReSharper disable once UnusedParameter.Local
#pragma warning disable RCS1163 // Unused parameter
- public InterpolatedSqlStatement(Int32 literalLength, Int32 formattedCount)
+ public InterpolatedSqlStatement(int literalLength, int formattedCount)
#pragma warning restore RCS1163 // Unused parameter
{
this.fragments = new(formattedCount);
@@ -66,12 +69,15 @@ public InterpolatedSqlStatement(Int32 literalLength, Int32 formattedCount)
/// If a parameter value is an , it is serialized according to
/// .
///
- public InterpolatedSqlStatement(String code, params (String Name, Object? Value)[] parameters)
+ public InterpolatedSqlStatement(string code, params (string Name, object? Value)[] parameters)
{
ArgumentNullException.ThrowIfNull(code);
ArgumentNullException.ThrowIfNull(parameters);
- this.fragments = new(1 /* fragment for the code */ + parameters.Length /* fragments for the parameters */);
+ this.fragments = new(
+ 1 /* fragment for the code */
+ + parameters.Length /* fragments for the parameters */
+ );
this.temporaryTables = [];
this.fragments.Add(new Literal(code));
@@ -86,8 +92,8 @@ public InterpolatedSqlStatement(String code, params (String Name, Object? Value)
var duplicateParameterNames = duplicateParameters.SelectMany(a => a.Select(b => $"'{b.Name}'")).ToList();
throw new ArgumentException(
- "The specified parameters have the following duplicate parameter names: " +
- $"{String.Join(", ", duplicateParameterNames)}. Make sure each parameter name is only used once.",
+ "The specified parameters have the following duplicate parameter names: "
+ + $"{string.Join(", ", duplicateParameterNames)}. Make sure each parameter name is only used once.",
nameof(parameters)
);
}
@@ -99,98 +105,71 @@ public InterpolatedSqlStatement(String code, params (String Name, Object? Value)
}
///
- /// Appends the specified value to this instance.
+ /// The fragments that make up this SQL statement.
///
- /// The type of value to append.
- /// The value to append.
- ///
- /// The minimum number of characters that should be written for .
- /// A negative value indicates that the value should be left-aligned and the required minimum whitespace characters
- /// to add is the absolute value.
- ///
- /// The string to use to format .
- ///
- /// This method is part of the interpolated string handler pattern.
- /// It is not intended to be called by user code.
- ///
- [EditorBrowsable(EditorBrowsableState.Never)]
- public void AppendFormatted(T? value, Int32 alignment = 0, String? format = null)
- {
- switch (value)
- {
- case InterpolatedParameter interpolatedParameter:
- this.fragments.Add(interpolatedParameter);
- break;
-
- case InterpolatedTemporaryTable interpolatedTemporaryTable:
- this.fragments.Add(interpolatedTemporaryTable);
- this.temporaryTables.Add(interpolatedTemporaryTable);
- break;
+ internal IReadOnlyList Fragments => this.fragments;
- default:
- var formattedValue =
- value switch
- {
- String stringValue => stringValue,
- IFormattable formattable => formattable.ToString(format, CultureInfo.InvariantCulture),
- null => String.Empty,
- _ => value.ToString() ?? String.Empty
- };
+ ///
+ /// The temporary tables used in this SQL statement.
+ ///
+ internal readonly IReadOnlyList TemporaryTables => this.temporaryTables;
- if (alignment != 0)
- {
- var paddingWidth = Math.Abs(alignment);
- var padding = paddingWidth - formattedValue.Length;
+ ///
+ /// Implicitly converts a string to an instance of .
+ ///
+ /// The string to convert to an instance of .
+ /// is .
+ public static implicit operator InterpolatedSqlStatement(string value)
+ {
+ ArgumentNullException.ThrowIfNull(value);
- if (padding > 0)
- {
- if (alignment > 0)
- {
- // Right-align:
- this.fragments.Add(new Literal(new String(' ', padding) + formattedValue));
- }
- else
- {
- // Left-align:
- this.fragments.Add(new Literal(formattedValue + new String(' ', padding)));
- }
+ return new(value);
+ }
- break;
- }
- }
+ ///
+ /// Determines whether the two specified instances of are equal.
+ ///
+ /// The first instance to compare.
+ /// The second instance to compare.
+ ///
+ /// if the two specified instances of are
+ /// equal; otherwise, .
+ ///
+ public static bool operator ==(InterpolatedSqlStatement left, InterpolatedSqlStatement right) => left.Equals(right);
- this.fragments.Add(new Literal(formattedValue));
- break;
- }
- }
+ ///
+ /// Determines whether the two specified instances of are unequal.
+ ///
+ /// The first instance to compare.
+ /// The second instance to compare.
+ ///
+ /// if the two the specified instances of are
+ /// unequal; otherwise, .
+ ///
+ public static bool operator !=(InterpolatedSqlStatement left, InterpolatedSqlStatement right) => !(left == right);
///
- /// Appends the specified literal value to this instance.
+ /// Creates a new instance of from the specified string.
///
- /// The literal value to append to this instance.
- ///
- /// This method is part of the interpolated string handler pattern.
- /// It is not intended to be called by user code.
- ///
- [EditorBrowsable(EditorBrowsableState.Never)]
- public void AppendLiteral(String? value)
+ ///
+ /// The string from which to create an instance of .
+ ///
+ /// is .
+ public static InterpolatedSqlStatement FromString(string value)
{
- if (value is not null)
- {
- this.fragments.Add(new Literal(value));
- }
+ ArgumentNullException.ThrowIfNull(value);
+
+ return new(value);
}
///
- public readonly Boolean Equals(InterpolatedSqlStatement other) =>
- this.fragments.SequenceEqual(other.Fragments);
+ public readonly bool Equals(InterpolatedSqlStatement other) => this.fragments.SequenceEqual(other.Fragments);
///
- public readonly override Boolean Equals(Object? obj) =>
- obj is InterpolatedSqlStatement other && this.Equals(other);
+ public override readonly bool Equals(object? obj) => obj is InterpolatedSqlStatement other && this.Equals(other);
///
- public readonly override Int32 GetHashCode()
+ public override readonly int GetHashCode()
{
var hashCode = new HashCode();
@@ -203,9 +182,9 @@ public readonly override Int32 GetHashCode()
}
///
- public readonly override String ToString()
+ public override readonly string ToString()
{
- using var stringBuilder = new ValueStringBuilder(stackalloc Char[500]);
+ using var stringBuilder = new ValueStringBuilder(stackalloc char[500]);
stringBuilder.AppendLine("SQL Statement");
stringBuilder.AppendLine("");
@@ -213,7 +192,7 @@ public readonly override String ToString()
stringBuilder.AppendLine("Statement Code");
stringBuilder.AppendLine("--------------");
- var parameters = new Dictionary(StringComparer.Ordinal);
+ var parameters = new Dictionary(StringComparer.Ordinal);
var interpolatedTemporaryTables = new List();
foreach (var fragment in this.fragments)
@@ -227,7 +206,7 @@ public readonly override String ToString()
case InterpolatedParameter interpolatedParameter:
var parameterName = interpolatedParameter.InferredName;
- if (String.IsNullOrWhiteSpace(parameterName))
+ if (string.IsNullOrWhiteSpace(parameterName))
{
parameterName = "Parameter_" + (parameters.Count + 1).ToString(CultureInfo.InvariantCulture);
}
@@ -286,7 +265,7 @@ public readonly override String ToString()
{
stringBuilder.AppendLine();
stringBuilder.AppendLine(temporaryTable.Name);
- stringBuilder.AppendLine(new String('-', temporaryTable.Name.Length));
+ stringBuilder.AppendLine(new string('-', temporaryTable.Name.Length));
foreach (var value in temporaryTable.Values)
{
@@ -298,65 +277,84 @@ public readonly override String ToString()
}
///
- /// Creates a new instance of from the specified string.
+ /// Appends the specified value to this instance.
///
- ///
- /// The string from which to create an instance of .
+ /// The type of value to append.
+ /// The value to append.
+ ///
+ /// The minimum number of characters that should be written for .
+ /// A negative value indicates that the value should be left-aligned and the required minimum whitespace characters
+ /// to add is the absolute value.
///
- /// is .
- public static InterpolatedSqlStatement FromString(String value)
+ /// The string to use to format .
+ ///
+ /// This method is part of the interpolated string handler pattern.
+ /// It is not intended to be called by user code.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public void AppendFormatted(T? value, int alignment = 0, string? format = null)
{
- ArgumentNullException.ThrowIfNull(value);
+ switch (value)
+ {
+ case InterpolatedParameter interpolatedParameter:
+ this.fragments.Add(interpolatedParameter);
+ break;
- return new(value);
- }
+ case InterpolatedTemporaryTable interpolatedTemporaryTable:
+ this.fragments.Add(interpolatedTemporaryTable);
+ this.temporaryTables.Add(interpolatedTemporaryTable);
+ break;
- ///
- /// Determines whether the two specified instances of are equal.
- ///
- /// The first instance to compare.
- /// The second instance to compare.
- ///
- /// if the two specified instances of are
- /// equal; otherwise, .
- ///
- public static Boolean operator ==(InterpolatedSqlStatement left, InterpolatedSqlStatement right) =>
- left.Equals(right);
+ default:
+ var formattedValue = value switch
+ {
+ string stringValue => stringValue,
+ IFormattable formattable => formattable.ToString(format, CultureInfo.InvariantCulture),
+ null => string.Empty,
+ _ => value.ToString() ?? string.Empty,
+ };
- ///
- /// Implicitly converts a string to an instance of .
- ///
- /// The string to convert to an instance of .
- /// is .
- public static implicit operator InterpolatedSqlStatement(String value)
- {
- ArgumentNullException.ThrowIfNull(value);
+ if (alignment != 0)
+ {
+ var paddingWidth = Math.Abs(alignment);
+ var padding = paddingWidth - formattedValue.Length;
- return new(value);
- }
+ if (padding > 0)
+ {
+ if (alignment > 0)
+ {
+ // Right-align:
+ this.fragments.Add(new Literal(new string(' ', padding) + formattedValue));
+ }
+ else
+ {
+ // Left-align:
+ this.fragments.Add(new Literal(formattedValue + new string(' ', padding)));
+ }
- ///
- /// Determines whether the two specified instances of are unequal.
- ///
- /// The first instance to compare.
- /// The second instance to compare.
- ///
- /// if the two the specified instances of are
- /// unequal; otherwise, .
- ///
- public static Boolean operator !=(InterpolatedSqlStatement left, InterpolatedSqlStatement right) =>
- !(left == right);
+ break;
+ }
+ }
- ///
- /// The fragments that make up this SQL statement.
- ///
- internal IReadOnlyList Fragments => this.fragments;
+ this.fragments.Add(new Literal(formattedValue));
+ break;
+ }
+ }
///
- /// The temporary tables used in this SQL statement.
+ /// Appends the specified literal value to this instance.
///
- internal readonly IReadOnlyList TemporaryTables => this.temporaryTables;
-
- private readonly List fragments;
- private readonly List temporaryTables;
+ /// The literal value to append to this instance.
+ ///
+ /// This method is part of the interpolated string handler pattern.
+ /// It is not intended to be called by user code.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public void AppendLiteral(string? value)
+ {
+ if (value is not null)
+ {
+ this.fragments.Add(new Literal(value));
+ }
+ }
}
diff --git a/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatementDebugView.cs b/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatementDebugView.cs
index 81edad7..4002c3b 100644
--- a/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatementDebugView.cs
+++ b/src/DbConnectionPlus/SqlStatements/InterpolatedSqlStatementDebugView.cs
@@ -12,12 +12,10 @@ internal sealed class InterpolatedSqlStatementDebugView(InterpolatedSqlStatement
///
/// The debug view of the SQL statement.
///
- public String DebugView =>
- statement.ToString();
+ public string DebugView => statement.ToString();
///
/// The fragments that make up the SQL statement.
///
- public IReadOnlyList Fragments =>
- statement.Fragments;
+ public IReadOnlyList Fragments => statement.Fragments;
}
diff --git a/src/DbConnectionPlus/SqlStatements/InterpolatedTemporaryTable.cs b/src/DbConnectionPlus/SqlStatements/InterpolatedTemporaryTable.cs
index 368902d..892bd15 100644
--- a/src/DbConnectionPlus/SqlStatements/InterpolatedTemporaryTable.cs
+++ b/src/DbConnectionPlus/SqlStatements/InterpolatedTemporaryTable.cs
@@ -16,10 +16,9 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements;
/// property the trimmer would not see that requirement.
///
public record InterpolatedTemporaryTable(
- String Name,
+ string Name,
IEnumerable Values,
[property: DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
[param: DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)]
- Type ValuesType
-)
- : IInterpolatedSqlStatementFragment;
+ Type ValuesType
+) : IInterpolatedSqlStatementFragment;
diff --git a/src/DbConnectionPlus/SqlStatements/Literal.cs b/src/DbConnectionPlus/SqlStatements/Literal.cs
index 209e528..67371ef 100644
--- a/src/DbConnectionPlus/SqlStatements/Literal.cs
+++ b/src/DbConnectionPlus/SqlStatements/Literal.cs
@@ -7,4 +7,4 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements;
/// A fragment of an interpolated SQL statement that represents a literal string.
///
/// The literal string.
-internal record Literal(String Value) : IInterpolatedSqlStatementFragment;
+internal record Literal(string Value) : IInterpolatedSqlStatementFragment;
diff --git a/src/DbConnectionPlus/SqlStatements/Parameter.cs b/src/DbConnectionPlus/SqlStatements/Parameter.cs
index aa5cf34..2cfa0d1 100644
--- a/src/DbConnectionPlus/SqlStatements/Parameter.cs
+++ b/src/DbConnectionPlus/SqlStatements/Parameter.cs
@@ -8,4 +8,4 @@ namespace RentADeveloper.DbConnectionPlus.SqlStatements;
///
/// The name of the parameter.
/// The value of the parameter.
-internal record Parameter(String Name, Object? Value) : IInterpolatedSqlStatementFragment;
+internal record Parameter(string Name, object? Value) : IInterpolatedSqlStatementFragment;
diff --git a/src/DbConnectionPlus/ThrowHelper.cs b/src/DbConnectionPlus/ThrowHelper.cs
index 7048eb8..12c9416 100644
--- a/src/DbConnectionPlus/ThrowHelper.cs
+++ b/src/DbConnectionPlus/ThrowHelper.cs
@@ -1,7 +1,6 @@
// Copyright (c) 2026 David Liebeherr
// Licensed under the MIT License. See LICENSE.md in the project root for more information.
-using System.Diagnostics.CodeAnalysis;
using RentADeveloper.DbConnectionPlus.Exceptions;
using RentADeveloper.DbConnectionPlus.Extensions;
@@ -36,9 +35,9 @@ public static void ThrowConfigurationIsFrozenException() =>
public static void ThrowDatabaseAdapterDoesNotSupportTemporaryTablesException(IDatabaseAdapter databaseAdapter) =>
throw new NotSupportedException(
#pragma warning disable CA1062
- $"The database adapter {databaseAdapter.GetType()} does not support (local / session-scoped) " +
- "temporary tables. Therefore the temporary tables feature of DbConnectionPlus can not be used with " +
- "this database."
+ $"The database adapter {databaseAdapter.GetType()} does not support (local / session-scoped) "
+ + "temporary tables. Therefore the temporary tables feature of DbConnectionPlus can not be used with "
+ + "this database."
#pragma warning restore CA1062
);
@@ -55,15 +54,15 @@ public static void ThrowDatabaseAdapterDoesNotSupportTemporaryTablesException(ID
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
public static void ThrowDatabaseOperationAffectedUnexpectedNumberOfRowsException(
- Int32 expectedNumberOfAffectedRows,
- Int32 actualNumberOfAffectedRows,
- Object entity
+ int expectedNumberOfAffectedRows,
+ int actualNumberOfAffectedRows,
+ object entity
) =>
throw new DbUpdateConcurrencyException(
- $"The database operation was expected to affect {expectedNumberOfAffectedRows} row(s), but actually " +
- $"affected {actualNumberOfAffectedRows} row(s). Data in the database may have been modified or deleted " +
- $"since entities were loaded. See {nameof(DbUpdateConcurrencyException)}." +
- $"{nameof(DbUpdateConcurrencyException.Entity)} for the entity that was involved in the operation.",
+ $"The database operation was expected to affect {expectedNumberOfAffectedRows} row(s), but actually "
+ + $"affected {actualNumberOfAffectedRows} row(s). Data in the database may have been modified or deleted "
+ + $"since entities were loaded. See {nameof(DbUpdateConcurrencyException)}."
+ + $"{nameof(DbUpdateConcurrencyException.Entity)} for the entity that was involved in the operation.",
entity
);
@@ -76,8 +75,8 @@ Object entity
[DoesNotReturn]
public static void ThrowEntityTypeHasNoKeyPropertyException(Type entityType) =>
throw new ArgumentException(
- $"No property of the type {entityType} is configured as a key property. Make sure that at least one " +
- "instance property of that type is configured as key property."
+ $"No property of the type {entityType} is configured as a key property. Make sure that at least one "
+ + "instance property of that type is configured as key property."
);
///
@@ -134,13 +133,12 @@ public static void ThrowSqlStatementReturnedNoRowsException() =>
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
public static T ThrowWrongConnectionTypeException()
- where TExpectedConnectionType : DbConnection
- =>
- throw new ArgumentOutOfRangeException(
- // ReSharper disable once NotResolvedInText
- "connection",
- $"The provided connection is not of the type {typeof(TExpectedConnectionType)}."
- );
+ where TExpectedConnectionType : DbConnection =>
+ throw new ArgumentOutOfRangeException(
+ // ReSharper disable once NotResolvedInText
+ "connection",
+ $"The provided connection is not of the type {typeof(TExpectedConnectionType)}."
+ );
///
/// Throws an indicating that the specified transaction is not of the
@@ -156,11 +154,10 @@ public static T ThrowWrongConnectionTypeException()
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
public static T ThrowWrongTransactionTypeException()
- where TExpectedTransactionType : DbTransaction
- =>
- throw new ArgumentOutOfRangeException(
- // ReSharper disable once NotResolvedInText
- "transaction",
- $"The provided transaction is not of the type {typeof(TExpectedTransactionType)}."
- );
+ where TExpectedTransactionType : DbTransaction =>
+ throw new ArgumentOutOfRangeException(
+ // ReSharper disable once NotResolvedInText
+ "transaction",
+ $"The provided transaction is not of the type {typeof(TExpectedTransactionType)}."
+ );
}
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 7429cb4..32008c3 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -4,11 +4,12 @@
Shared build properties for the shipping libraries.
The repository-root Directory.Build.props carries what every project needs (authorship, language
- settings, the style analyzers). MSBuild uses the nearest Directory.Build.props and stops, so this file
- imports it explicitly - without that import, the projects under src/ would silently lose all of it.
+ settings, the style analyzers, and the style gate itself - EnforceCodeStyleInBuild with
+ TreatWarningsAsErrors). MSBuild uses the nearest Directory.Build.props and stops, so this file imports
+ it explicitly - without that import, the projects under src/ would silently lose all of it.
- What lives here is what only a shipping, packable library needs: multi-targeting, the AOT and public-API
- analyzers, warnings-as-errors, and the NuGet package metadata. Per-project identity - AssemblyName,
+ What lives here is what only a shipping, packable library needs: multi-targeting, the AOT, CA and
+ public-API analyzers, and the NuGet package metadata. Per-project identity - AssemblyName,
AssemblyTitle, RootNamespace, PackageId, Description, PackageTags - stays in each .csproj.
-->
@@ -44,10 +45,13 @@
+
latest-allTrue
- True
- true
diff --git a/stylecop.json b/stylecop.json
new file mode 100644
index 0000000..fea9d81
--- /dev/null
+++ b/stylecop.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "orderingRules": {
+ "elementOrder": ["kind", "accessibility", "constant", "static", "readonly"],
+ "systemUsingDirectivesFirst": true,
+ "usingDirectivesPlacement": "outsideNamespace"
+ }
+ }
+}
diff --git a/tests/DbConnectionPlus.IntegrationTests/Assertions/EntityAssertions.cs b/tests/DbConnectionPlus.IntegrationTests/Assertions/EntityAssertions.cs
index e8c1ac2..af40db0 100644
--- a/tests/DbConnectionPlus.IntegrationTests/Assertions/EntityAssertions.cs
+++ b/tests/DbConnectionPlus.IntegrationTests/Assertions/EntityAssertions.cs
@@ -12,64 +12,44 @@ public static class EntityAssertions
///
/// The data row to assert.
/// The entity to assert against.
- public static void AssertDataRowMatchesEntity(
- DataRow dataRow,
- Entity entity
- )
+ public static void AssertDataRowMatchesEntity(DataRow dataRow, Entity entity)
{
// We need to use the ValueConverter here because each database provider handles the types a bit
// differently.
- ValueConverter.ConvertValueToType(dataRow["BooleanValue"])
- .Should().Be(entity.BooleanValue);
+ ValueConverter.ConvertValueToType(dataRow["BooleanValue"]).Should().Be(entity.BooleanValue);
- ValueConverter.ConvertValueToType(dataRow["ByteValue"])
- .Should().Be(entity.ByteValue);
+ ValueConverter.ConvertValueToType(dataRow["ByteValue"]).Should().Be(entity.ByteValue);
- ValueConverter.ConvertValueToType(dataRow["CharValue"])
- .Should().Be(entity.CharValue);
+ ValueConverter.ConvertValueToType(dataRow["CharValue"]).Should().Be(entity.CharValue);
- ValueConverter.ConvertValueToType(dataRow["DateOnlyValue"])
- .Should().Be(entity.DateOnlyValue);
+ ValueConverter.ConvertValueToType(dataRow["DateOnlyValue"]).Should().Be(entity.DateOnlyValue);
- ValueConverter.ConvertValueToType(dataRow["DateTimeValue"])
- .Should().Be(entity.DateTimeValue);
+ ValueConverter.ConvertValueToType(dataRow["DateTimeValue"]).Should().Be(entity.DateTimeValue);
- ValueConverter.ConvertValueToType(dataRow["DecimalValue"])
- .Should().Be(entity.DecimalValue);
+ ValueConverter.ConvertValueToType(dataRow["DecimalValue"]).Should().Be(entity.DecimalValue);
- ValueConverter.ConvertValueToType(dataRow["DoubleValue"])
- .Should().Be(entity.DoubleValue);
+ ValueConverter.ConvertValueToType(dataRow["DoubleValue"]).Should().Be(entity.DoubleValue);
- ValueConverter.ConvertValueToType(dataRow["EnumValue"])
- .Should().Be(entity.EnumValue);
+ ValueConverter.ConvertValueToType(dataRow["EnumValue"]).Should().Be(entity.EnumValue);
- ValueConverter.ConvertValueToType(dataRow["GuidValue"])
- .Should().Be(entity.GuidValue);
+ ValueConverter.ConvertValueToType(dataRow["GuidValue"]).Should().Be(entity.GuidValue);
- ValueConverter.ConvertValueToType(dataRow["Id"])
- .Should().Be(entity.Id);
+ ValueConverter.ConvertValueToType(dataRow["Id"]).Should().Be(entity.Id);
- ValueConverter.ConvertValueToType(dataRow["Int16Value"])
- .Should().Be(entity.Int16Value);
+ ValueConverter.ConvertValueToType(dataRow["Int16Value"]).Should().Be(entity.Int16Value);
- ValueConverter.ConvertValueToType(dataRow["Int32Value"])
- .Should().Be(entity.Int32Value);
+ ValueConverter.ConvertValueToType(dataRow["Int32Value"]).Should().Be(entity.Int32Value);
- ValueConverter.ConvertValueToType(dataRow["Int64Value"])
- .Should().Be(entity.Int64Value);
+ ValueConverter.ConvertValueToType(dataRow["Int64Value"]).Should().Be(entity.Int64Value);
- ValueConverter.ConvertValueToType(dataRow["SingleValue"])
- .Should().Be(entity.SingleValue);
+ ValueConverter.ConvertValueToType(dataRow["SingleValue"]).Should().Be(entity.SingleValue);
- ValueConverter.ConvertValueToType(dataRow["StringValue"])
- .Should().Be(entity.StringValue);
+ ValueConverter.ConvertValueToType(dataRow["StringValue"]).Should().Be(entity.StringValue);
- ValueConverter.ConvertValueToType(dataRow["TimeOnlyValue"])
- .Should().Be(entity.TimeOnlyValue);
+ ValueConverter.ConvertValueToType(dataRow["TimeOnlyValue"]).Should().Be(entity.TimeOnlyValue);
- ValueConverter.ConvertValueToType(dataRow["TimeSpanValue"])
- .Should().Be(entity.TimeSpanValue);
+ ValueConverter.ConvertValueToType(dataRow["TimeSpanValue"]).Should().Be(entity.TimeSpanValue);
}
///
@@ -77,10 +57,7 @@ Entity entity
///
/// The list of data rows to assert.
/// The list of entities to assert against.
- public static void AssertDataRowsMatchEntities(
- List dataRows,
- List entities
- )
+ public static void AssertDataRowsMatchEntities(List dataRows, List entities)
{
for (var i = 0; i < entities.Count; i++)
{
diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs
index 06a5a13..9b0d897 100644
--- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs
+++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntitiesTests.cs
@@ -4,40 +4,34 @@
namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters;
-public sealed class
- EntityManipulator_DeleteEntitiesTests_MySql :
- EntityManipulator_DeleteEntitiesTests;
+public sealed class EntityManipulator_DeleteEntitiesTests_MySql
+ : EntityManipulator_DeleteEntitiesTests;
-public sealed class
- EntityManipulator_DeleteEntitiesTests_Oracle :
- EntityManipulator_DeleteEntitiesTests;
+public sealed class EntityManipulator_DeleteEntitiesTests_Oracle
+ : EntityManipulator_DeleteEntitiesTests;
-public sealed class
- EntityManipulator_DeleteEntitiesTests_PostgreSql :
- EntityManipulator_DeleteEntitiesTests;
+public sealed class EntityManipulator_DeleteEntitiesTests_PostgreSql
+ : EntityManipulator_DeleteEntitiesTests;
-public sealed class
- EntityManipulator_DeleteEntitiesTests_Sqlite :
- EntityManipulator_DeleteEntitiesTests;
+public sealed class EntityManipulator_DeleteEntitiesTests_Sqlite
+ : EntityManipulator_DeleteEntitiesTests;
-public sealed class
- EntityManipulator_DeleteEntitiesTests_SqlServer :
- EntityManipulator_DeleteEntitiesTests;
+public sealed class EntityManipulator_DeleteEntitiesTests_SqlServer
+ : EntityManipulator_DeleteEntitiesTests;
-public abstract class EntityManipulator_DeleteEntitiesTests
- : IntegrationTestsBase
+public abstract class EntityManipulator_DeleteEntitiesTests
+ : IntegrationTestsBase
where TTestDatabaseProvider : ITestDatabaseProvider, new()
{
+ private readonly IEntityManipulator manipulator;
+
///
- protected EntityManipulator_DeleteEntitiesTests() =>
- this.manipulator = this.DatabaseAdapter.EntityManipulator;
+ protected EntityManipulator_DeleteEntitiesTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator;
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(
- Boolean useAsyncApi
- )
+ public async Task DeleteEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi)
{
Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, "");
@@ -48,36 +42,31 @@ Boolean useAsyncApi
this.DelayNextDbCommand = true;
- await Invoking(() => this.CallApi(
- useAsyncApi,
- this.Connection,
- entitiesToDelete,
- null,
- cancellationToken
- )
- )
- .Should().ThrowAsync()
+ await Invoking(() => this.CallApi(useAsyncApi, this.Connection, entitiesToDelete, null, cancellationToken))
+ .Should()
+ .ThrowAsync()
.Where(a => a.CancellationToken == cancellationToken);
foreach (var entity in entities)
{
// Since the operation was cancelled, all entities should still exist.
- this.ExistsEntityInDb(entity)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entity).Should().BeTrue();
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntities_ConcurrencyTokenMismatch_ShouldThrow(Boolean useAsyncApi)
+ public async Task DeleteEntities_ConcurrencyTokenMismatch_ShouldThrow(bool useAsyncApi)
{
var entitiesToDelete = this.CreateEntitiesInDb(5);
var failingEntity = entitiesToDelete[^1];
- failingEntity.ConcurrencyToken_ = Generate.Single();
+ failingEntity.ConcurrencyToken_ = Generate.Single();
- var exception = (await Invoking(() => this.CallApi(
+ var exception = (
+ await Invoking(() =>
+ this.CallApi(
useAsyncApi,
this.Connection,
entitiesToDelete,
@@ -85,64 +74,55 @@ public async Task DeleteEntities_ConcurrencyTokenMismatch_ShouldThrow(Boolean us
TestContext.Current.CancellationToken
)
)
- .Should().ThrowAsync())
- .Subject.First();
-
- exception.Message
- .Should().Be(
- "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " +
- "Data in the database may have been modified or deleted since entities were loaded. See " +
- $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " +
- "the entity that was involved in the operation."
+ .Should()
+ .ThrowAsync()
+ ).Subject.First();
+
+ exception
+ .Message.Should()
+ .Be(
+ "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). "
+ + "Data in the database may have been modified or deleted since entities were loaded. See "
+ + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for "
+ + "the entity that was involved in the operation."
);
- exception.Entity
- .Should().Be(failingEntity);
+ exception.Entity.Should().Be(failingEntity);
foreach (var entity in entitiesToDelete.Except([failingEntity]))
{
- this.ExistsEntityInDb(entity)
- .Should().BeFalse();
+ this.ExistsEntityInDb(entity).Should().BeFalse();
}
- this.ExistsEntityInDb(failingEntity)
- .Should().BeTrue();
+ this.ExistsEntityInDb(failingEntity).Should().BeTrue();
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntities_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi)
+ public async Task DeleteEntities_Mapping_Attributes_ShouldUseAttributesMapping(bool useAsyncApi)
{
var entities = this.CreateEntitiesInDb(10);
var entitiesToDelete = entities.Take(5).ToList();
var entitiesToKeep = entities.Skip(5).ToList();
- await this.CallApi(
- useAsyncApi,
- this.Connection,
- entitiesToDelete,
- null,
- TestContext.Current.CancellationToken
- );
+ await this.CallApi(useAsyncApi, this.Connection, entitiesToDelete, null, TestContext.Current.CancellationToken);
foreach (var entity in entitiesToDelete)
{
- this.ExistsEntityInDb(entity)
- .Should().BeFalse();
+ this.ExistsEntityInDb(entity).Should().BeFalse();
}
foreach (var entity in entitiesToKeep)
{
- this.ExistsEntityInDb(entity)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entity).Should().BeTrue();
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi)
+ public async Task DeleteEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(bool useAsyncApi)
{
MappingTestEntityFluentApi.Configure();
@@ -150,35 +130,28 @@ public async Task DeleteEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boo
var entitiesToDelete = entities.Take(5).ToList();
var entitiesToKeep = entities.Skip(5).ToList();
- await this.CallApi(
- useAsyncApi,
- this.Connection,
- entitiesToDelete,
- null,
- TestContext.Current.CancellationToken
- );
+ await this.CallApi(useAsyncApi, this.Connection, entitiesToDelete, null, TestContext.Current.CancellationToken);
foreach (var entity in entitiesToDelete)
{
- this.ExistsEntityInDb(entity)
- .Should().BeFalse();
+ this.ExistsEntityInDb(entity).Should().BeFalse();
}
foreach (var entity in entitiesToKeep)
{
- this.ExistsEntityInDb(entity)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entity).Should().BeTrue();
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public Task DeleteEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi)
+ public Task DeleteEntities_Mapping_MissingKeyProperty_ShouldThrow(bool useAsyncApi)
{
var entityWithoutKeyProperty = new EntityWithoutKeyProperty();
- return Invoking(() => this.CallApi(
+ return Invoking(() =>
+ this.CallApi(
useAsyncApi,
this.Connection,
[entityWithoutKeyProperty],
@@ -186,114 +159,115 @@ public Task DeleteEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsy
TestContext.Current.CancellationToken
)
)
- .Should().ThrowAsync()
+ .Should()
+ .ThrowAsync()
.WithMessage(
- $"No property of the type {typeof(EntityWithoutKeyProperty)} is configured as a key property. Make " +
- "sure that at least one instance property of that type is configured as key property."
+ $"No property of the type {typeof(EntityWithoutKeyProperty)} is configured as a key property. Make "
+ + "sure that at least one instance property of that type is configured as key property."
);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi)
+ public async Task DeleteEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(bool useAsyncApi)
{
var entities = this.CreateEntitiesInDb(10);
var entitiesToDelete = entities.Take(5).ToList();
var entitiesToKeep = entities.Skip(5).ToList();
- await this.CallApi(
- useAsyncApi,
- this.Connection,
- entitiesToDelete,
- null,
- TestContext.Current.CancellationToken
- );
+ await this.CallApi(useAsyncApi, this.Connection, entitiesToDelete, null, TestContext.Current.CancellationToken);
foreach (var entity in entitiesToDelete)
{
- this.ExistsEntityInDb(entity)
- .Should().BeFalse();
+ this.ExistsEntityInDb(entity).Should().BeFalse();
}
foreach (var entity in entitiesToKeep)
{
- this.ExistsEntityInDb(entity)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entity).Should().BeTrue();
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntities_RowVersionMismatch_ShouldThrow(Boolean useAsyncApi)
+ public async Task DeleteEntities_RowVersionMismatch_ShouldThrow(bool useAsyncApi)
{
var entitiesToDelete = this.CreateEntitiesInDb(5);
var failingEntity = entitiesToDelete[^1];
- failingEntity.RowVersion_ = Generate.Single();
+ failingEntity.RowVersion_ = Generate.Single();
- var exception = (await Invoking(() => this.CallApi(
- useAsyncApi,
- this.Connection,
- entitiesToDelete,
- null,
- TestContext.Current.CancellationToken
+ var exception = (
+ await Invoking(() =>
+ this.CallApi(
+ useAsyncApi,
+ this.Connection,
+ entitiesToDelete,
+ null,
+ TestContext.Current.CancellationToken
+ )
)
- )
- .Should().ThrowAsync()).Subject.First();
-
- exception.Message
- .Should().Be(
- "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " +
- "Data in the database may have been modified or deleted since entities were loaded. See " +
- $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " +
- "the entity that was involved in the operation."
+ .Should()
+ .ThrowAsync()
+ ).Subject.First();
+
+ exception
+ .Message.Should()
+ .Be(
+ "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). "
+ + "Data in the database may have been modified or deleted since entities were loaded. See "
+ + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for "
+ + "the entity that was involved in the operation."
);
- exception.Entity
- .Should().Be(failingEntity);
+ exception.Entity.Should().Be(failingEntity);
foreach (var entity in entitiesToDelete.Except([failingEntity]))
{
- this.ExistsEntityInDb(entity)
- .Should().BeFalse();
+ this.ExistsEntityInDb(entity).Should().BeFalse();
}
- this.ExistsEntityInDb(failingEntity)
- .Should().BeTrue();
+ this.ExistsEntityInDb(failingEntity).Should().BeTrue();
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntities_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi)
+ public async Task DeleteEntities_ShouldReturnNumberOfAffectedRows(bool useAsyncApi)
{
var entitiesToDelete = this.CreateEntitiesInDb();
- (await this.CallApi(
+ (
+ await this.CallApi(
useAsyncApi,
this.Connection,
entitiesToDelete,
null,
TestContext.Current.CancellationToken
- ))
- .Should().Be(entitiesToDelete.Count);
+ )
+ )
+ .Should()
+ .Be(entitiesToDelete.Count);
- (await this.CallApi(
+ (
+ await this.CallApi(
useAsyncApi,
this.Connection,
Array.Empty(),
null,
TestContext.Current.CancellationToken
- ))
- .Should().Be(0);
+ )
+ )
+ .Should()
+ .Be(0);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntities_Transaction_ShouldUseTransaction(Boolean useAsyncApi)
+ public async Task DeleteEntities_Transaction_ShouldUseTransaction(bool useAsyncApi)
{
var entitiesToDelete = this.CreateEntitiesInDb();
@@ -309,8 +283,7 @@ await this.CallApi(
foreach (var entity in entitiesToDelete)
{
- this.ExistsEntityInDb(entity, transaction)
- .Should().BeFalse();
+ this.ExistsEntityInDb(entity, transaction).Should().BeFalse();
}
await transaction.RollbackAsync();
@@ -318,13 +291,12 @@ await this.CallApi(
foreach (var entity in entitiesToDelete)
{
- this.ExistsEntityInDb(entity)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entity).Should().BeTrue();
}
}
- private Task CallApi(
- Boolean useAsyncApi,
+ private Task CallApi(
+ bool useAsyncApi,
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction = null,
@@ -345,9 +317,7 @@ private Task CallApi(
}
catch (Exception ex)
{
- return Task.FromException(ex);
+ return Task.FromException(ex);
}
}
-
- private readonly IEntityManipulator manipulator;
}
diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs
index f4003f1..00b1476 100644
--- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs
+++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.DeleteEntityTests.cs
@@ -4,38 +4,34 @@
namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters;
-public sealed class
- EntityManipulator_DeleteEntityTests_MySql :
- EntityManipulator_DeleteEntityTests;
+public sealed class EntityManipulator_DeleteEntityTests_MySql
+ : EntityManipulator_DeleteEntityTests;
-public sealed class
- EntityManipulator_DeleteEntityTests_Oracle :
- EntityManipulator_DeleteEntityTests;
+public sealed class EntityManipulator_DeleteEntityTests_Oracle
+ : EntityManipulator_DeleteEntityTests;
-public sealed class
- EntityManipulator_DeleteEntityTests_PostgreSql :
- EntityManipulator_DeleteEntityTests;
+public sealed class EntityManipulator_DeleteEntityTests_PostgreSql
+ : EntityManipulator_DeleteEntityTests;
-public sealed class
- EntityManipulator_DeleteEntityTests_Sqlite :
- EntityManipulator_DeleteEntityTests;
+public sealed class EntityManipulator_DeleteEntityTests_Sqlite
+ : EntityManipulator_DeleteEntityTests;
-public sealed class
- EntityManipulator_DeleteEntityTests_SqlServer :
- EntityManipulator_DeleteEntityTests;
+public sealed class EntityManipulator_DeleteEntityTests_SqlServer
+ : EntityManipulator_DeleteEntityTests;
-public abstract class EntityManipulator_DeleteEntityTests
- : IntegrationTestsBase
+public abstract class EntityManipulator_DeleteEntityTests
+ : IntegrationTestsBase
where TTestDatabaseProvider : ITestDatabaseProvider, new()
{
+ private readonly IEntityManipulator manipulator;
+
///
- protected EntityManipulator_DeleteEntityTests() =>
- this.manipulator = this.DatabaseAdapter.EntityManipulator;
+ protected EntityManipulator_DeleteEntityTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator;
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi)
+ public async Task DeleteEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi)
{
Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, "");
@@ -45,83 +41,71 @@ public async Task DeleteEntity_CancellationToken_ShouldCancelOperationIfCancella
this.DelayNextDbCommand = true;
- await Invoking(() => this.CallApi(
- useAsyncApi,
- this.Connection,
- entityToDelete,
- null,
- cancellationToken
- )
- )
- .Should().ThrowAsync()
+ await Invoking(() => this.CallApi(useAsyncApi, this.Connection, entityToDelete, null, cancellationToken))
+ .Should()
+ .ThrowAsync()
.Where(a => a.CancellationToken == cancellationToken);
// Since the operation was cancelled, the entity should still exist.
- this.ExistsEntityInDb(entityToDelete)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entityToDelete).Should().BeTrue();
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntity_ConcurrencyTokenMismatch_ShouldThrow(Boolean useAsyncApi)
+ public async Task DeleteEntity_ConcurrencyTokenMismatch_ShouldThrow(bool useAsyncApi)
{
var entityToDelete = this.CreateEntityInDb();
- entityToDelete.ConcurrencyToken_ = Generate.Single();
-
- var exception = (await Invoking(() => this.CallApi(
- useAsyncApi,
- this.Connection,
- entityToDelete,
- null,
- TestContext.Current.CancellationToken
+ entityToDelete.ConcurrencyToken_ = Generate.Single();
+
+ var exception = (
+ await Invoking(() =>
+ this.CallApi(
+ useAsyncApi,
+ this.Connection,
+ entityToDelete,
+ null,
+ TestContext.Current.CancellationToken
+ )
)
- )
- .Should().ThrowAsync()).Subject.First();
-
- exception.Message
- .Should().Be(
- "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " +
- "Data in the database may have been modified or deleted since entities were loaded. See " +
- $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " +
- "the entity that was involved in the operation."
+ .Should()
+ .ThrowAsync()
+ ).Subject.First();
+
+ exception
+ .Message.Should()
+ .Be(
+ "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). "
+ + "Data in the database may have been modified or deleted since entities were loaded. See "
+ + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for "
+ + "the entity that was involved in the operation."
);
- exception.Entity
- .Should().Be(entityToDelete);
+ exception.Entity.Should().Be(entityToDelete);
- this.ExistsEntityInDb(entityToDelete)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entityToDelete).Should().BeTrue();
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi)
+ public async Task DeleteEntity_Mapping_Attributes_ShouldUseAttributesMapping(bool useAsyncApi)
{
var entities = this.CreateEntitiesInDb(2);
var entityToDelete = entities[0];
var entityToKeep = entities[1];
- await this.CallApi(
- useAsyncApi,
- this.Connection,
- entityToDelete,
- null,
- TestContext.Current.CancellationToken
- );
+ await this.CallApi(useAsyncApi, this.Connection, entityToDelete, null, TestContext.Current.CancellationToken);
- this.ExistsEntityInDb(entityToDelete)
- .Should().BeFalse();
+ this.ExistsEntityInDb(entityToDelete).Should().BeFalse();
- this.ExistsEntityInDb(entityToKeep)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entityToKeep).Should().BeTrue();
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi)
+ public async Task DeleteEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(bool useAsyncApi)
{
MappingTestEntityFluentApi.Configure();
@@ -129,29 +113,22 @@ public async Task DeleteEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boole
var entityToDelete = entities[0];
var entityToKeep = entities[1];
- await this.CallApi(
- useAsyncApi,
- this.Connection,
- entityToDelete,
- null,
- TestContext.Current.CancellationToken
- );
+ await this.CallApi(useAsyncApi, this.Connection, entityToDelete, null, TestContext.Current.CancellationToken);
- this.ExistsEntityInDb(entityToDelete)
- .Should().BeFalse();
+ this.ExistsEntityInDb(entityToDelete).Should().BeFalse();
- this.ExistsEntityInDb(entityToKeep)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entityToKeep).Should().BeTrue();
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public Task DeleteEntity_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi)
+ public Task DeleteEntity_Mapping_MissingKeyProperty_ShouldThrow(bool useAsyncApi)
{
var entityWithoutKeyProperty = new EntityWithoutKeyProperty();
- return Invoking(() => this.CallApi(
+ return Invoking(() =>
+ this.CallApi(
useAsyncApi,
this.Connection,
entityWithoutKeyProperty,
@@ -159,91 +136,82 @@ public Task DeleteEntity_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsync
TestContext.Current.CancellationToken
)
)
- .Should().ThrowAsync()
+ .Should()
+ .ThrowAsync()
.WithMessage(
- $"No property of the type {typeof(EntityWithoutKeyProperty)} is configured as a key property. Make " +
- "sure that at least one instance property of that type is configured as key property."
+ $"No property of the type {typeof(EntityWithoutKeyProperty)} is configured as a key property. Make "
+ + "sure that at least one instance property of that type is configured as key property."
);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi)
+ public async Task DeleteEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(bool useAsyncApi)
{
var entities = this.CreateEntitiesInDb(2);
var entityToDelete = entities[0];
var entityToKeep = entities[1];
- await this.CallApi(
- useAsyncApi,
- this.Connection,
- entityToDelete,
- null,
- TestContext.Current.CancellationToken
- );
+ await this.CallApi(useAsyncApi, this.Connection, entityToDelete, null, TestContext.Current.CancellationToken);
- this.ExistsEntityInDb(entityToDelete)
- .Should().BeFalse();
+ this.ExistsEntityInDb(entityToDelete).Should().BeFalse();
- this.ExistsEntityInDb(entityToKeep)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entityToKeep).Should().BeTrue();
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntity_RowVersionMismatch_ShouldThrow(Boolean useAsyncApi)
+ public async Task DeleteEntity_RowVersionMismatch_ShouldThrow(bool useAsyncApi)
{
var entityToDelete = this.CreateEntityInDb();
- entityToDelete.RowVersion_ = Generate.Single();
-
- var exception = (await Invoking(() => this.CallApi(
- useAsyncApi,
- this.Connection,
- entityToDelete,
- null,
- TestContext.Current.CancellationToken
+ entityToDelete.RowVersion_ = Generate.Single();
+
+ var exception = (
+ await Invoking(() =>
+ this.CallApi(
+ useAsyncApi,
+ this.Connection,
+ entityToDelete,
+ null,
+ TestContext.Current.CancellationToken
+ )
)
- )
- .Should().ThrowAsync()).Subject.First();
-
- exception.Message
- .Should().Be(
- "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). " +
- "Data in the database may have been modified or deleted since entities were loaded. See " +
- $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for " +
- "the entity that was involved in the operation."
+ .Should()
+ .ThrowAsync()
+ ).Subject.First();
+
+ exception
+ .Message.Should()
+ .Be(
+ "The database operation was expected to affect 1 row(s), but actually affected 0 row(s). "
+ + "Data in the database may have been modified or deleted since entities were loaded. See "
+ + $"{nameof(DbUpdateConcurrencyException)}.{nameof(DbUpdateConcurrencyException.Entity)} for "
+ + "the entity that was involved in the operation."
);
- exception.Entity
- .Should().Be(entityToDelete);
+ exception.Entity.Should().Be(entityToDelete);
- this.ExistsEntityInDb(entityToDelete)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entityToDelete).Should().BeTrue();
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntity_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi)
+ public async Task DeleteEntity_ShouldReturnNumberOfAffectedRows(bool useAsyncApi)
{
var entityToDelete = this.CreateEntityInDb();
- (await this.CallApi(
- useAsyncApi,
- this.Connection,
- entityToDelete,
- null,
- TestContext.Current.CancellationToken
- ))
- .Should().Be(1);
+ (await this.CallApi(useAsyncApi, this.Connection, entityToDelete, null, TestContext.Current.CancellationToken))
+ .Should()
+ .Be(1);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task DeleteEntity_Transaction_ShouldUseTransaction(Boolean useAsyncApi)
+ public async Task DeleteEntity_Transaction_ShouldUseTransaction(bool useAsyncApi)
{
var entityToDelete = this.CreateEntityInDb();
@@ -257,18 +225,16 @@ await this.CallApi(
TestContext.Current.CancellationToken
);
- this.ExistsEntityInDb(entityToDelete, transaction)
- .Should().BeFalse();
+ this.ExistsEntityInDb(entityToDelete, transaction).Should().BeFalse();
await transaction.RollbackAsync();
}
- this.ExistsEntityInDb(entityToDelete)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entityToDelete).Should().BeTrue();
}
- private Task CallApi(
- Boolean useAsyncApi,
+ private Task CallApi(
+ bool useAsyncApi,
DbConnection connection,
TEntity entity,
DbTransaction? transaction = null,
@@ -283,15 +249,11 @@ private Task CallApi(
try
{
- return Task.FromResult(
- this.manipulator.DeleteEntity(connection, entity, transaction, cancellationToken)
- );
+ return Task.FromResult(this.manipulator.DeleteEntity(connection, entity, transaction, cancellationToken));
}
catch (Exception ex)
{
- return Task.FromException(ex);
+ return Task.FromException(ex);
}
}
-
- private readonly IEntityManipulator manipulator;
}
diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs
index 185ec00..a662448 100644
--- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs
+++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntitiesTests.cs
@@ -3,40 +3,34 @@
namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters;
-public sealed class
- EntityManipulator_InsertEntitiesTests_MySql :
- EntityManipulator_InsertEntitiesTests;
+public sealed class EntityManipulator_InsertEntitiesTests_MySql
+ : EntityManipulator_InsertEntitiesTests;
-public sealed class
- EntityManipulator_InsertEntitiesTests_Oracle :
- EntityManipulator_InsertEntitiesTests;
+public sealed class EntityManipulator_InsertEntitiesTests_Oracle
+ : EntityManipulator_InsertEntitiesTests;
-public sealed class
- EntityManipulator_InsertEntitiesTests_PostgreSql :
- EntityManipulator_InsertEntitiesTests;
+public sealed class EntityManipulator_InsertEntitiesTests_PostgreSql
+ : EntityManipulator_InsertEntitiesTests;
-public sealed class
- EntityManipulator_InsertEntitiesTests_Sqlite :
- EntityManipulator_InsertEntitiesTests;
+public sealed class EntityManipulator_InsertEntitiesTests_Sqlite
+ : EntityManipulator_InsertEntitiesTests;
-public sealed class
- EntityManipulator_InsertEntitiesTests_SqlServer :
- EntityManipulator_InsertEntitiesTests;
+public sealed class EntityManipulator_InsertEntitiesTests_SqlServer
+ : EntityManipulator_InsertEntitiesTests;
-public abstract class EntityManipulator_InsertEntitiesTests
- : IntegrationTestsBase
+public abstract class EntityManipulator_InsertEntitiesTests
+ : IntegrationTestsBase
where TTestDatabaseProvider : ITestDatabaseProvider, new()
{
+ private readonly IEntityManipulator manipulator;
+
///
- protected EntityManipulator_InsertEntitiesTests() =>
- this.manipulator = this.DatabaseAdapter.EntityManipulator;
+ protected EntityManipulator_InsertEntitiesTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator;
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task InsertEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(
- Boolean useAsyncApi
- )
+ public async Task InsertEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi)
{
Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, "");
@@ -46,246 +40,226 @@ Boolean useAsyncApi
this.DelayNextDbCommand = true;
- await Invoking(() =>
- this.CallApi(useAsyncApi, this.Connection, entities, null, cancellationToken)
- )
- .Should().ThrowAsync()
+ await Invoking(() => this.CallApi(useAsyncApi, this.Connection, entities, null, cancellationToken))
+ .Should()
+ .ThrowAsync()
.Where(a => a.CancellationToken == cancellationToken);
// Since the operation was cancelled, the entities should not have been inserted.
foreach (var entityToInsert in entities)
{
- this.ExistsEntityInDb(entityToInsert)
- .Should().BeFalse();
+ this.ExistsEntityInDb(entityToInsert).Should().BeFalse();
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task InsertEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers(
- Boolean useAsyncApi
- )
+ public async Task InsertEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers(bool useAsyncApi)
{
DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers;
var entities = Generate.Multiple();
- await this.CallApi(
- useAsyncApi,
- this.Connection,
- entities,
- null,
- TestContext.Current.CancellationToken
- );
-
- (await this.Connection.QueryAsync(
- $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}",
- cancellationToken: TestContext.Current.CancellationToken
- ).ToListAsync(TestContext.Current.CancellationToken))
- .Should().BeEquivalentTo(entities.Select(a => (Int32)a.Enum));
+ await this.CallApi(useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken);
+
+ (
+ await this
+ .Connection.QueryAsync(
+ $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}",
+ cancellationToken: TestContext.Current.CancellationToken
+ )
+ .ToListAsync(TestContext.Current.CancellationToken)
+ )
+ .Should()
+ .BeEquivalentTo(entities.Select(a => (int)a.Enum));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task InsertEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi)
+ public async Task InsertEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(bool useAsyncApi)
{
DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings;
var entities = Generate.Multiple();
- await this.CallApi(
- useAsyncApi,
- this.Connection,
- entities,
- null,
- TestContext.Current.CancellationToken
- );
-
- (await this.Connection.QueryAsync(
- $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}",
- cancellationToken: TestContext.Current.CancellationToken
- ).ToListAsync(TestContext.Current.CancellationToken))
- .Should().BeEquivalentTo(entities.Select(a => a.Enum.ToString()));
+ await this.CallApi(useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken);
+
+ (
+ await this
+ .Connection.QueryAsync(
+ $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}",
+ cancellationToken: TestContext.Current.CancellationToken
+ )
+ .ToListAsync(TestContext.Current.CancellationToken)
+ )
+ .Should()
+ .BeEquivalentTo(entities.Select(a => a.Enum.ToString()));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task InsertEntities_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi)
+ public async Task InsertEntities_Mapping_Attributes_ShouldUseAttributesMapping(bool useAsyncApi)
{
var entities = Generate.Multiple();
entities.ForEach(a =>
- {
- a.Computed_ = 0;
- a.Identity_ = 0;
- a.NotMapped = "ShouldNotBePersisted";
- }
- );
+ {
+ a.Computed_ = 0;
+ a.Identity_ = 0;
+ a.NotMapped = "ShouldNotBePersisted";
+ });
- await this.CallApi(
- useAsyncApi,
- this.Connection,
- entities,
- null,
- TestContext.Current.CancellationToken
- );
+ await this.CallApi(useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken);
this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}")
- .Should().BeEquivalentTo(
+ .Should()
+ .BeEquivalentTo(
entities,
- options => options.Using(context => context.Subject.Should().BeNull())
- .When(info => info.Path.EndsWith("NotMapped"))
+ options =>
+ options
+ .Using(context => context.Subject.Should().BeNull())
+ .When(info => info.Path.EndsWith("NotMapped"))
);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task InsertEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi)
+ public async Task InsertEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(bool useAsyncApi)
{
MappingTestEntityFluentApi.Configure();
var entities = Generate.Multiple();
entities.ForEach(a =>
- {
- a.Computed_ = 0;
- a.Identity_ = 0;
- a.NotMapped = "ShouldNotBePersisted";
- }
- );
+ {
+ a.Computed_ = 0;
+ a.Identity_ = 0;
+ a.NotMapped = "ShouldNotBePersisted";
+ });
- await this.CallApi(
- useAsyncApi,
- this.Connection,
- entities,
- null,
- TestContext.Current.CancellationToken
- );
+ await this.CallApi(useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken);
this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}")
- .Should().BeEquivalentTo(
+ .Should()
+ .BeEquivalentTo(
entities,
- options => options.Using(context => context.Subject.Should().BeNull())
- .When(info => info.Path.EndsWith("NotMapped"))
+ options =>
+ options
+ .Using(context => context.Subject.Should().BeNull())
+ .When(info => info.Path.EndsWith("NotMapped"))
);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task InsertEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi)
+ public async Task InsertEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(bool useAsyncApi)
{
var entities = Generate.Multiple();
- await this.CallApi(
- useAsyncApi,
- this.Connection,
- entities,
- null,
- TestContext.Current.CancellationToken
- );
+ await this.CallApi(useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken);
this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}")
- .Should().BeEquivalentTo(entities);
+ .Should()
+ .BeEquivalentTo(entities);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task InsertEntities_ShouldInsertEntities(Boolean useAsyncApi)
+ public async Task InsertEntities_ShouldInsertEntities(bool useAsyncApi)
{
var entities = Generate.Multiple();
- (await this.CallApi(
- useAsyncApi,
- this.Connection,
- entities,
- null,
- TestContext.Current.CancellationToken
- ))
- .Should().Be(entities.Count);
-
- (await this.Connection.QueryAsync(
- $"SELECT * FROM {Q("Entity")}",
- cancellationToken: TestContext.Current.CancellationToken
- ).ToListAsync(TestContext.Current.CancellationToken))
- .Should().BeEquivalentTo(entities);
+ (await this.CallApi(useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken))
+ .Should()
+ .Be(entities.Count);
+
+ (
+ await this
+ .Connection.QueryAsync(
+ $"SELECT * FROM {Q("Entity")}",
+ cancellationToken: TestContext.Current.CancellationToken
+ )
+ .ToListAsync(TestContext.Current.CancellationToken)
+ )
+ .Should()
+ .BeEquivalentTo(entities);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task InsertEntities_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi)
+ public async Task InsertEntities_ShouldReturnNumberOfAffectedRows(bool useAsyncApi)
{
var entities = Generate.Multiple();
- (await this.CallApi(
- useAsyncApi,
- this.Connection,
- entities,
- null,
- TestContext.Current.CancellationToken
- ))
- .Should().Be(entities.Count);
+ (await this.CallApi(useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken))
+ .Should()
+ .Be(entities.Count);
- (await this.CallApi(
+ (
+ await this.CallApi(
useAsyncApi,
this.Connection,
Array.Empty(),
null,
TestContext.Current.CancellationToken
- ))
- .Should().Be(0);
+ )
+ )
+ .Should()
+ .Be(0);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task InsertEntities_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi)
+ public async Task InsertEntities_ShouldSupportDateTimeOffsetValues(bool useAsyncApi)
{
Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, "");
var entities = Generate.Multiple();
- await this.CallApi(
- useAsyncApi,
- this.Connection,
- entities,
- null,
- TestContext.Current.CancellationToken
- );
-
- (await this.Connection.QueryAsync(
- $"SELECT * FROM {Q("EntityWithDateTimeOffset")}",
- cancellationToken: TestContext.Current.CancellationToken
- ).ToListAsync(TestContext.Current.CancellationToken))
- .Should().BeEquivalentTo(entities);
+ await this.CallApi(useAsyncApi, this.Connection, entities, null, TestContext.Current.CancellationToken);
+
+ (
+ await this
+ .Connection.QueryAsync(
+ $"SELECT * FROM {Q("EntityWithDateTimeOffset")}",
+ cancellationToken: TestContext.Current.CancellationToken
+ )
+ .ToListAsync(TestContext.Current.CancellationToken)
+ )
+ .Should()
+ .BeEquivalentTo(entities);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
- public async Task InsertEntities_Transaction_ShouldUseTransaction(Boolean useAsyncApi)
+ public async Task InsertEntities_Transaction_ShouldUseTransaction(bool useAsyncApi)
{
var entities = Generate.Multiple();
await using (var transaction = await this.Connection.BeginTransactionAsync())
{
- (await this.CallApi(
+ (
+ await this.CallApi(
useAsyncApi,
this.Connection,
entities,
transaction,
TestContext.Current.CancellationToken
- ))
- .Should().Be(entities.Count);
+ )
+ )
+ .Should()
+ .Be(entities.Count);
foreach (var entity in entities)
{
- this.ExistsEntityInDb(entity, transaction)
- .Should().BeTrue();
+ this.ExistsEntityInDb(entity, transaction).Should().BeTrue();
}
await transaction.RollbackAsync();
@@ -293,13 +267,12 @@ public async Task InsertEntities_Transaction_ShouldUseTransaction(Boolean useAsy
foreach (var entity in entities)
{
- this.ExistsEntityInDb(entity)
- .Should().BeFalse();
+ this.ExistsEntityInDb(entity).Should().BeFalse();
}
}
- private Task CallApi(
- Boolean useAsyncApi,
+ private Task CallApi(
+ bool useAsyncApi,
DbConnection connection,
IEnumerable entities,
DbTransaction? transaction = null,
@@ -320,9 +293,7 @@ private Task CallApi